From 1984caa91b47b7c12aa34b096ef2720888b02770 Mon Sep 17 00:00:00 2001
From: Claudio Olmi
Date: Tue, 21 May 2024 23:56:54 -0500
Subject: [PATCH 01/41] update
---
PYPI.md | 2 ++
README.md | 1 +
functional_test.py | 26 +++++++++++++++++++++-----
pyxtream/pyxtream.py | 5 +++--
pyxtream/version.py | 2 +-
setup.py | 6 +++++-
6 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/PYPI.md b/PYPI.md
index 5416f75..e0c515a 100644
--- a/PYPI.md
+++ b/PYPI.md
@@ -1,11 +1,13 @@
# Build PIP Module
python3 setup.py sdist bdist_wheel
+python3 -m build
# Upload to PYPI
twine upload dist/pyxtream-0.7*
# Optional Local Install
python3 -m pip install dist/pyxtream-0.7
+python3 -m pip install --editable dist/pyxtream-0.7
# GitHub Documentation
diff --git a/README.md b/README.md
index c816f68..79a049f 100644
--- a/README.md
+++ b/README.md
@@ -112,6 +112,7 @@ xTream.movies[{},{},...]
| Date | Version | Description |
| ----------- | -----| ----------- |
+| 2024-05-21 | 0.7.1 | - Fixed missing jsonschema package - Fixed provider name in functional_test
| 2023-11-08 | 0.7.0 | - Added Schema Validator - Added Channel Age - Added list of movies added in the last 30 and 7 days - Updated code based on PyLint - Fixed Flask package to be optional [richard-de-vos](https://github.com/richard-de-vos)|
| 2023-02-06 | 0.6.0 | - Added methods to change connection header, to turn off reload timer, and to enable/disable Flask debug mode - Added a loop when attempting to connect to the provider - Cleaned up some print lines|
| 2021-08-19 | 0.5.0 | - Added method to gracefully handle connection errors - Added setting to not load adult content - Added sorting by stream name - Changed the handling of special characters in streams - Changed print formatting - Changed index.html webpage to HTML5 and Bootstrap 5|
diff --git a/functional_test.py b/functional_test.py
index 7e2b16f..4fb2320 100755
--- a/functional_test.py
+++ b/functional_test.py
@@ -5,10 +5,10 @@
from pyxtream import XTream, __version__
-PROVIDER_NAME = ""
-PROVIDER_URL = ""
-PROVIDER_USERNAME = ""
-PROVIDER_PASSWORD = ""
+PROVIDER_NAME = "Alibaba"
+PROVIDER_URL = "http://megamegeric.xyz:80"
+PROVIDER_USERNAME = "6580771576"
+PROVIDER_PASSWORD = "1839762243"
if PROVIDER_URL == "" or PROVIDER_USERNAME == "" or PROVIDER_PASSWORD == "":
print("Please edit this file with the provider credentials")
@@ -40,7 +40,7 @@ def str2list(input_string: str) -> list:
print(f"pyxtream version {__version__}")
xt = XTream(
- "YourProvider",
+ PROVIDER_NAME,
PROVIDER_USERNAME,
PROVIDER_PASSWORD,
PROVIDER_URL,
@@ -66,6 +66,8 @@ def str2list(input_string: str) -> list:
(3) Search Streams Text
(4) Download Video (stream_id)
(5) Download Video Impl (URL, filename)
+ (6) Show how many movies added in past 30 days
+ (7) Show how many movies added in past 7 days
----------
(0) Quit
"""
@@ -123,3 +125,17 @@ def str2list(input_string: str) -> list:
url = input("Enter URL to download: ")
filename = input("Enter Fullpath Filename: ")
xt._download_video_impl(url,filename)
+
+ elif choice == 6:
+ num_movies = len(xt.movies_30days)
+ print(f"Found {num_movies} new movies in the past 30 days")
+ if num_movies < 20:
+ for i in range(0,num_movies):
+ print(xt.movies_30days[i].title)
+
+ elif choice == 7:
+ num_movies = len(xt.movies_7days)
+ print(f"Found {num_movies} new movies in the past 7 days")
+ if num_movies < 20:
+ for i in range(0,num_movies):
+ print(xt.movies_7days[i].title)
diff --git a/pyxtream/pyxtream.py b/pyxtream/pyxtream.py
index c51374c..b1b24fb 100755
--- a/pyxtream/pyxtream.py
+++ b/pyxtream/pyxtream.py
@@ -583,6 +583,7 @@ def authenticate(self):
r = None
# Prepare the authentication url
url = f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ print(f"Attempting connection: ", end='')
while i < 30:
try:
# Request authentication, wait 4 seconds maximum
@@ -590,7 +591,7 @@ def authenticate(self):
i = 31
except requests.exceptions.ConnectionError:
time.sleep(1)
- print(i)
+ print(f"{i} ", end='',flush=True)
i += 1
if r is not None:
@@ -612,7 +613,7 @@ def authenticate(self):
else:
print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code} {r.reason}`")
else:
- print(f"{self.name}: Provider refused the connection")
+ print(f"\n{self.name}: Provider refused the connection")
def _load_from_file(self, filename) -> dict:
"""Try to load the dictionary from file
diff --git a/pyxtream/version.py b/pyxtream/version.py
index 39cdad1..cc06b4b 100644
--- a/pyxtream/version.py
+++ b/pyxtream/version.py
@@ -1,4 +1,4 @@
-__version__ = '0.7.0'
+__version__ = '0.7.1'
__author__ = 'Claudio Olmi'
__author_email__ = 'superolmo2@gmail.com'
diff --git a/setup.py b/setup.py
index 4f55a92..919b03b 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,8 @@
-from setuptools import setup, find_packages
from distutils.util import convert_path
+from setuptools import find_packages, setup
+
with open("README.md", "r") as fh:
long_description = fh.read()
@@ -30,6 +31,9 @@
"Operating System :: OS Independent",
"Natural Language :: English"
],
+ install_require=[
+ 'jsonschema'
+ ],
extras_require={
"REST_API": ["Flask>=1.1.2",],
}
From 4d50da641db928ab0c3a7320d68364dad4e3a080 Mon Sep 17 00:00:00 2001
From: Claudio Olmi
Date: Tue, 21 May 2024 23:58:23 -0500
Subject: [PATCH 02/41] Removed test
---
functional_test.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/functional_test.py b/functional_test.py
index 4fb2320..1111916 100755
--- a/functional_test.py
+++ b/functional_test.py
@@ -5,10 +5,10 @@
from pyxtream import XTream, __version__
-PROVIDER_NAME = "Alibaba"
-PROVIDER_URL = "http://megamegeric.xyz:80"
-PROVIDER_USERNAME = "6580771576"
-PROVIDER_PASSWORD = "1839762243"
+PROVIDER_NAME = ""
+PROVIDER_URL = ""
+PROVIDER_USERNAME = ""
+PROVIDER_PASSWORD = ""
if PROVIDER_URL == "" or PROVIDER_USERNAME == "" or PROVIDER_PASSWORD == "":
print("Please edit this file with the provider credentials")
From e3b177481314b81c9c0fcb0a53da37bb12b846da Mon Sep 17 00:00:00 2001
From: Claudio Olmi
Date: Wed, 22 May 2024 00:01:40 -0500
Subject: [PATCH 03/41] Update
---
README.md | 2 +-
setup.py | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 79a049f..94def52 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,7 @@ xTream.movies[{},{},...]
| Date | Version | Description |
| ----------- | -----| ----------- |
-| 2024-05-21 | 0.7.1 | - Fixed missing jsonschema package - Fixed provider name in functional_test
+| 2024-05-21 | 0.7.1 | - Fixed missing jsonschema package - Fixed provider name in functional_test - Improved print out of connection attempts
| 2023-11-08 | 0.7.0 | - Added Schema Validator - Added Channel Age - Added list of movies added in the last 30 and 7 days - Updated code based on PyLint - Fixed Flask package to be optional [richard-de-vos](https://github.com/richard-de-vos)|
| 2023-02-06 | 0.6.0 | - Added methods to change connection header, to turn off reload timer, and to enable/disable Flask debug mode - Added a loop when attempting to connect to the provider - Cleaned up some print lines|
| 2021-08-19 | 0.5.0 | - Added method to gracefully handle connection errors - Added setting to not load adult content - Added sorting by stream name - Changed the handling of special characters in streams - Changed print formatting - Changed index.html webpage to HTML5 and Bootstrap 5|
diff --git a/setup.py b/setup.py
index 919b03b..70f5138 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,6 @@
-from distutils.util import convert_path
-
from setuptools import find_packages, setup
+from distutils.util import convert_path
with open("README.md", "r") as fh:
long_description = fh.read()
From 0e198f6d58d8a8644644cb019dd64a6991528857 Mon Sep 17 00:00:00 2001
From: Claudio Olmi
Date: Wed, 22 May 2024 00:06:30 -0500
Subject: [PATCH 04/41] Update
---
README.md | 2 +-
setup.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 94def52..b869167 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,7 @@ xTream.movies[{},{},...]
| Date | Version | Description |
| ----------- | -----| ----------- |
-| 2024-05-21 | 0.7.1 | - Fixed missing jsonschema package - Fixed provider name in functional_test - Improved print out of connection attempts
+| 2024-05-21 | 0.7.1 | - Fixed missing jsonschema package - Fixed provider name in functional_test - Improved print out of connection attempts - Added method to read latest changes in functional_test
| 2023-11-08 | 0.7.0 | - Added Schema Validator - Added Channel Age - Added list of movies added in the last 30 and 7 days - Updated code based on PyLint - Fixed Flask package to be optional [richard-de-vos](https://github.com/richard-de-vos)|
| 2023-02-06 | 0.6.0 | - Added methods to change connection header, to turn off reload timer, and to enable/disable Flask debug mode - Added a loop when attempting to connect to the provider - Cleaned up some print lines|
| 2021-08-19 | 0.5.0 | - Added method to gracefully handle connection errors - Added setting to not load adult content - Added sorting by stream name - Changed the handling of special characters in streams - Changed print formatting - Changed index.html webpage to HTML5 and Bootstrap 5|
diff --git a/setup.py b/setup.py
index 70f5138..4824f89 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,5 @@
-from setuptools import find_packages, setup
+from setuptools import setup, find_packages
from distutils.util import convert_path
with open("README.md", "r") as fh:
From 337576736739b60d8945c93dcbbaf582f4dbdd97 Mon Sep 17 00:00:00 2001
From: Claudio Olmi
Date: Wed, 22 May 2024 00:35:14 -0500
Subject: [PATCH 05/41] Update
---
PYPI.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/PYPI.md b/PYPI.md
index e0c515a..5416f75 100644
--- a/PYPI.md
+++ b/PYPI.md
@@ -1,13 +1,11 @@
# Build PIP Module
python3 setup.py sdist bdist_wheel
-python3 -m build
# Upload to PYPI
twine upload dist/pyxtream-0.7*
# Optional Local Install
python3 -m pip install dist/pyxtream-0.7
-python3 -m pip install --editable dist/pyxtream-0.7
# GitHub Documentation
From cfe9b19bf1edc3a8b0dd9c33358a59d778d19b8e Mon Sep 17 00:00:00 2001
From: Claudio Olmi
Date: Wed, 22 May 2024 00:39:12 -0500
Subject: [PATCH 06/41] Updated docs
---
PYPI.md | 5 +-
doc/pyxtream/pyxtream.html | 2783 ++++++++++++++++++------------------
doc/pyxtream/version.html | 2 +-
3 files changed, 1396 insertions(+), 1394 deletions(-)
diff --git a/PYPI.md b/PYPI.md
index 5416f75..24ecfb7 100644
--- a/PYPI.md
+++ b/PYPI.md
@@ -10,9 +10,8 @@ python3 -m pip install dist/pyxtream-0.7
# GitHub Documentation
## Build docs
-pdoc --html pyxtream/ --force
-mv html/pyxtream/*.html doc
-rm -rf html
+rm -rf doc
+pdoc pyxtream
# Record TS Video
ffmpeg -y -i "(iptv url)" -c:v copy -c:a copy -map 0:v -map 0:a -t 00:00:30 "myrecording.ts" >"mylog.log" 2>&1
diff --git a/doc/pyxtream/pyxtream.html b/doc/pyxtream/pyxtream.html
index 1978b50..da43222 100644
--- a/doc/pyxtream/pyxtream.html
+++ b/doc/pyxtream/pyxtream.html
@@ -1022,547 +1022,548 @@
583r=None 584# Prepare the authentication url 585url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 586whilei<30:
- 587try:
- 588# Request authentication, wait 4 seconds maximum
- 589r=requests.get(url,timeout=(4),headers=self.connection_headers)
- 590i=31
- 591exceptrequests.exceptions.ConnectionError:
- 592time.sleep(1)
- 593print(i)
- 594i+=1
- 595
- 596ifrisnotNone:
- 597# If the answer is ok, process data and change state
- 598ifr.ok:
- 599self.auth_data=r.json()
- 600self.authorization={
- 601"username":self.auth_data["user_info"]["username"],
- 602"password":self.auth_data["user_info"]["password"]
- 603}
- 604# Mark connection authorized
- 605self.state["authenticated"]=True
- 606# Construct the base url for all requests
- 607self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 608# If there is a secure server connection, construct the base url SSL for all requests
- 609if"https_port"inself.auth_data["server_info"]:
- 610self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
- 611f"/player_api.php?username={self.username}&password={self.password}"
- 612else:
- 613print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
- 614else:
- 615print(f"{self.name}: Provider refused the connection")
- 616
- 617def_load_from_file(self,filename)->dict:
- 618"""Try to load the dictionary from file
- 619
- 620 Args:
- 621 filename ([type]): File name containing the data
- 622
- 623 Returns:
- 624 dict: Dictionary if found and no errors, None if file does not exists
- 625 """
- 626# Build the full path
- 627full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 628
- 629# If the cached file exists, attempt to load it
- 630ifosp.isfile(full_filename):
- 631
- 632my_data=None
- 633
- 634# Get the enlapsed seconds since last file update
- 635file_age_sec=time.time()-osp.getmtime(full_filename)
- 636# If the file was updated less than the threshold time,
- 637# it means that the file is still fresh, we can load it.
- 638# Otherwise skip and return None to force a re-download
- 639ifself.threshold_time_sec>file_age_sec:
- 640# Load the JSON data
- 641try:
- 642withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
- 643my_data=json.load(myfile)
- 644iflen(my_data)==0:
- 645my_data=None
- 646exceptExceptionase:
- 647print(f" - Could not load from file `{full_filename}`: e=`{e}`")
- 648returnmy_data
- 649
- 650returnNone
- 651
- 652def_save_to_file(self,data_list:dict,filename:str)->bool:
- 653"""Save a dictionary to file
- 654
- 655 This function will overwrite the file if already exists
- 656
- 657 Args:
- 658 data_list (dict): Dictionary to save
- 659 filename (str): Name of the file
- 660
- 661 Returns:
- 662 bool: True if successfull, False if error
- 663 """
- 664ifdata_listisnotNone:
- 665
- 666#Build the full path
- 667full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 668# If the path makes sense, save the file
- 669json_data=json.dumps(data_list,ensure_ascii=False)
- 670try:
- 671withopen(full_filename,mode="wt",encoding="utf-8")asmyfile:
- 672myfile.write(json_data)
- 673exceptExceptionase:
- 674print(f" - Could not save to file `{full_filename}`: e=`{e}`")
- 675returnFalse
- 676
- 677returnTrue
- 678else:
- 679returnFalse
- 680
- 681defload_iptv(self)->bool:
- 682"""Load XTream IPTV
- 683
- 684 - Add all Live TV to XTream.channels
- 685 - Add all VOD to XTream.movies
- 686 - Add all Series to XTream.series
- 687 Series contains Seasons and Episodes. Those are not automatically
- 688 retrieved from the server to reduce the loading time.
- 689 - Add all groups to XTream.groups
- 690 Groups are for all three channel types, Live TV, VOD, and Series
- 691
- 692 Returns:
- 693 bool: True if successfull, False if error
- 694 """
- 695# If pyxtream has not authenticated the connection, return empty
- 696ifself.state["authenticated"]isFalse:
- 697print("Warning, cannot load steams since authorization failed")
- 698returnFalse
- 699
- 700# If pyxtream has already loaded the data, skip and return success
- 701ifself.state["loaded"]isTrue:
- 702print("Warning, data has already been loaded.")
- 703returnTrue
- 704
- 705forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
- 706## Get GROUPS
- 707
- 708# Try loading local file
- 709dt=0
- 710start=timer()
- 711all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
- 712# If file empty or does not exists, download it from remote
- 713ifall_catisNone:
- 714# Load all Groups and save file locally
- 715all_cat=self._load_categories_from_provider(loading_stream_type)
- 716ifall_catisnotNone:
- 717self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
- 718dt=timer()-start
- 719
- 720# If we got the GROUPS data, show the statistics and load GROUPS
- 721ifall_catisnotNone:
- 722print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
- 723## Add GROUPS to dictionaries
- 724
- 725# Add the catch-all-errors group
- 726ifloading_stream_type==self.live_type:
- 727self.groups.append(self.live_catch_all_group)
- 728elifloading_stream_type==self.vod_type:
- 729self.groups.append(self.vod_catch_all_group)
- 730elifloading_stream_type==self.series_type:
- 731self.groups.append(self.series_catch_all_group)
- 732
- 733forcat_objinall_cat:
- 734ifschemaValidator(cat_obj,SchemaType.GROUP):
- 735# Create Group (Category)
- 736new_group=Group(cat_obj,loading_stream_type)
- 737# Add to xtream class
- 738self.groups.append(new_group)
- 739else:
- 740# Save what did not pass schema validation
- 741print(cat_obj)
- 742
- 743# Sort Categories
- 744self.groups.sort(key=lambdax:x.name)
- 745else:
- 746print(f" - Could not load {loading_stream_type} Groups")
- 747break
- 748
- 749## Get Streams
- 750
- 751# Try loading local file
- 752dt=0
- 753start=timer()
- 754all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
- 755# If file empty or does not exists, download it from remote
- 756ifall_streamsisNone:
- 757# Load all Streams and save file locally
- 758all_streams=self._load_streams_from_provider(loading_stream_type)
- 759self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
- 760dt=timer()-start
- 761
- 762# If we got the STREAMS data, show the statistics and load Streams
- 763ifall_streamsisnotNone:
- 764print(
- 765f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams " \
- 766f"in {dt:.3f} seconds"
- 767)
- 768## Add Streams to dictionaries
- 769
- 770skipped_adult_content=0
- 771skipped_no_name_content=0
- 772
- 773number_of_streams=len(all_streams)
- 774current_stream_number=0
- 775# Calculate 1% of total number of streams
- 776# This is used to slow down the progress bar
- 777one_percent_number_of_streams=number_of_streams/100
- 778start=timer()
- 779forstream_channelinall_streams:
- 780skip_stream=False
- 781current_stream_number+=1
- 782
- 783# Show download progress every 1% of total number of streams
- 784ifcurrent_stream_number<one_percent_number_of_streams:
- 785progress(
- 786current_stream_number,
- 787number_of_streams,
- 788f"Processing {loading_stream_type} Streams"
- 789)
- 790one_percent_number_of_streams*=2
- 791
- 792# Validate JSON scheme
- 793ifself.validate_json:
- 794ifloading_stream_type==self.series_type:
- 795ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
- 796print(stream_channel)
- 797elifloading_stream_type==self.live_type:
- 798ifnotschemaValidator(stream_channel,SchemaType.LIVE):
- 799print(stream_channel)
- 800else:
- 801# vod_type
- 802ifnotschemaValidator(stream_channel,SchemaType.VOD):
- 803print(stream_channel)
- 804
- 805# Skip if the name of the stream is empty
- 806ifstream_channel["name"]=="":
- 807skip_stream=True
- 808skipped_no_name_content=skipped_no_name_content+1
- 809self._save_to_file_skipped_streams(stream_channel)
- 810
- 811# Skip if the user chose to hide adult streams
- 812ifself.hide_adult_contentandloading_stream_type==self.live_type:
- 813if"is_adult"instream_channel:
- 814ifstream_channel["is_adult"]=="1":
- 815skip_stream=True
- 816skipped_adult_content=skipped_adult_content+1
- 817self._save_to_file_skipped_streams(stream_channel)
- 818
- 819ifnotskip_stream:
- 820# Some channels have no group,
- 821# so let's add them to the catch all group
- 822ifstream_channel["category_id"]isNone:
- 823stream_channel["category_id"]="9999"
- 824elifstream_channel["category_id"]!="1":
- 825pass
- 826
- 827# Find the first occurence of the group that the
- 828# Channel or Stream is pointing to
- 829the_group=next(
- 830(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
- 831None
- 832)
- 833
- 834# Set group title
- 835ifthe_groupisnotNone:
- 836group_title=the_group.name
- 837else:
- 838ifloading_stream_type==self.live_type:
- 839group_title=self.live_catch_all_group.name
- 840the_group=self.live_catch_all_group
- 841elifloading_stream_type==self.vod_type:
- 842group_title=self.vod_catch_all_group.name
- 843the_group=self.vod_catch_all_group
- 844elifloading_stream_type==self.series_type:
- 845group_title=self.series_catch_all_group.name
- 846the_group=self.series_catch_all_group
- 847
+ 586print(f"Attempting connection: ",end='')
+ 587whilei<30:
+ 588try:
+ 589# Request authentication, wait 4 seconds maximum
+ 590r=requests.get(url,timeout=(4),headers=self.connection_headers)
+ 591i=31
+ 592exceptrequests.exceptions.ConnectionError:
+ 593time.sleep(1)
+ 594print(f"{i} ",end='',flush=True)
+ 595i+=1
+ 596
+ 597ifrisnotNone:
+ 598# If the answer is ok, process data and change state
+ 599ifr.ok:
+ 600self.auth_data=r.json()
+ 601self.authorization={
+ 602"username":self.auth_data["user_info"]["username"],
+ 603"password":self.auth_data["user_info"]["password"]
+ 604}
+ 605# Mark connection authorized
+ 606self.state["authenticated"]=True
+ 607# Construct the base url for all requests
+ 608self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 609# If there is a secure server connection, construct the base url SSL for all requests
+ 610if"https_port"inself.auth_data["server_info"]:
+ 611self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
+ 612f"/player_api.php?username={self.username}&password={self.password}"
+ 613else:
+ 614print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
+ 615else:
+ 616print(f"\n{self.name}: Provider refused the connection")
+ 617
+ 618def_load_from_file(self,filename)->dict:
+ 619"""Try to load the dictionary from file
+ 620
+ 621 Args:
+ 622 filename ([type]): File name containing the data
+ 623
+ 624 Returns:
+ 625 dict: Dictionary if found and no errors, None if file does not exists
+ 626 """
+ 627# Build the full path
+ 628full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 629
+ 630# If the cached file exists, attempt to load it
+ 631ifosp.isfile(full_filename):
+ 632
+ 633my_data=None
+ 634
+ 635# Get the enlapsed seconds since last file update
+ 636file_age_sec=time.time()-osp.getmtime(full_filename)
+ 637# If the file was updated less than the threshold time,
+ 638# it means that the file is still fresh, we can load it.
+ 639# Otherwise skip and return None to force a re-download
+ 640ifself.threshold_time_sec>file_age_sec:
+ 641# Load the JSON data
+ 642try:
+ 643withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
+ 644my_data=json.load(myfile)
+ 645iflen(my_data)==0:
+ 646my_data=None
+ 647exceptExceptionase:
+ 648print(f" - Could not load from file `{full_filename}`: e=`{e}`")
+ 649returnmy_data
+ 650
+ 651returnNone
+ 652
+ 653def_save_to_file(self,data_list:dict,filename:str)->bool:
+ 654"""Save a dictionary to file
+ 655
+ 656 This function will overwrite the file if already exists
+ 657
+ 658 Args:
+ 659 data_list (dict): Dictionary to save
+ 660 filename (str): Name of the file
+ 661
+ 662 Returns:
+ 663 bool: True if successfull, False if error
+ 664 """
+ 665ifdata_listisnotNone:
+ 666
+ 667#Build the full path
+ 668full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 669# If the path makes sense, save the file
+ 670json_data=json.dumps(data_list,ensure_ascii=False)
+ 671try:
+ 672withopen(full_filename,mode="wt",encoding="utf-8")asmyfile:
+ 673myfile.write(json_data)
+ 674exceptExceptionase:
+ 675print(f" - Could not save to file `{full_filename}`: e=`{e}`")
+ 676returnFalse
+ 677
+ 678returnTrue
+ 679else:
+ 680returnFalse
+ 681
+ 682defload_iptv(self)->bool:
+ 683"""Load XTream IPTV
+ 684
+ 685 - Add all Live TV to XTream.channels
+ 686 - Add all VOD to XTream.movies
+ 687 - Add all Series to XTream.series
+ 688 Series contains Seasons and Episodes. Those are not automatically
+ 689 retrieved from the server to reduce the loading time.
+ 690 - Add all groups to XTream.groups
+ 691 Groups are for all three channel types, Live TV, VOD, and Series
+ 692
+ 693 Returns:
+ 694 bool: True if successfull, False if error
+ 695 """
+ 696# If pyxtream has not authenticated the connection, return empty
+ 697ifself.state["authenticated"]isFalse:
+ 698print("Warning, cannot load steams since authorization failed")
+ 699returnFalse
+ 700
+ 701# If pyxtream has already loaded the data, skip and return success
+ 702ifself.state["loaded"]isTrue:
+ 703print("Warning, data has already been loaded.")
+ 704returnTrue
+ 705
+ 706forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
+ 707## Get GROUPS
+ 708
+ 709# Try loading local file
+ 710dt=0
+ 711start=timer()
+ 712all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
+ 713# If file empty or does not exists, download it from remote
+ 714ifall_catisNone:
+ 715# Load all Groups and save file locally
+ 716all_cat=self._load_categories_from_provider(loading_stream_type)
+ 717ifall_catisnotNone:
+ 718self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
+ 719dt=timer()-start
+ 720
+ 721# If we got the GROUPS data, show the statistics and load GROUPS
+ 722ifall_catisnotNone:
+ 723print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
+ 724## Add GROUPS to dictionaries
+ 725
+ 726# Add the catch-all-errors group
+ 727ifloading_stream_type==self.live_type:
+ 728self.groups.append(self.live_catch_all_group)
+ 729elifloading_stream_type==self.vod_type:
+ 730self.groups.append(self.vod_catch_all_group)
+ 731elifloading_stream_type==self.series_type:
+ 732self.groups.append(self.series_catch_all_group)
+ 733
+ 734forcat_objinall_cat:
+ 735ifschemaValidator(cat_obj,SchemaType.GROUP):
+ 736# Create Group (Category)
+ 737new_group=Group(cat_obj,loading_stream_type)
+ 738# Add to xtream class
+ 739self.groups.append(new_group)
+ 740else:
+ 741# Save what did not pass schema validation
+ 742print(cat_obj)
+ 743
+ 744# Sort Categories
+ 745self.groups.sort(key=lambdax:x.name)
+ 746else:
+ 747print(f" - Could not load {loading_stream_type} Groups")
+ 748break
+ 749
+ 750## Get Streams
+ 751
+ 752# Try loading local file
+ 753dt=0
+ 754start=timer()
+ 755all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
+ 756# If file empty or does not exists, download it from remote
+ 757ifall_streamsisNone:
+ 758# Load all Streams and save file locally
+ 759all_streams=self._load_streams_from_provider(loading_stream_type)
+ 760self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
+ 761dt=timer()-start
+ 762
+ 763# If we got the STREAMS data, show the statistics and load Streams
+ 764ifall_streamsisnotNone:
+ 765print(
+ 766f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams " \
+ 767f"in {dt:.3f} seconds"
+ 768)
+ 769## Add Streams to dictionaries
+ 770
+ 771skipped_adult_content=0
+ 772skipped_no_name_content=0
+ 773
+ 774number_of_streams=len(all_streams)
+ 775current_stream_number=0
+ 776# Calculate 1% of total number of streams
+ 777# This is used to slow down the progress bar
+ 778one_percent_number_of_streams=number_of_streams/100
+ 779start=timer()
+ 780forstream_channelinall_streams:
+ 781skip_stream=False
+ 782current_stream_number+=1
+ 783
+ 784# Show download progress every 1% of total number of streams
+ 785ifcurrent_stream_number<one_percent_number_of_streams:
+ 786progress(
+ 787current_stream_number,
+ 788number_of_streams,
+ 789f"Processing {loading_stream_type} Streams"
+ 790)
+ 791one_percent_number_of_streams*=2
+ 792
+ 793# Validate JSON scheme
+ 794ifself.validate_json:
+ 795ifloading_stream_type==self.series_type:
+ 796ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
+ 797print(stream_channel)
+ 798elifloading_stream_type==self.live_type:
+ 799ifnotschemaValidator(stream_channel,SchemaType.LIVE):
+ 800print(stream_channel)
+ 801else:
+ 802# vod_type
+ 803ifnotschemaValidator(stream_channel,SchemaType.VOD):
+ 804print(stream_channel)
+ 805
+ 806# Skip if the name of the stream is empty
+ 807ifstream_channel["name"]=="":
+ 808skip_stream=True
+ 809skipped_no_name_content=skipped_no_name_content+1
+ 810self._save_to_file_skipped_streams(stream_channel)
+ 811
+ 812# Skip if the user chose to hide adult streams
+ 813ifself.hide_adult_contentandloading_stream_type==self.live_type:
+ 814if"is_adult"instream_channel:
+ 815ifstream_channel["is_adult"]=="1":
+ 816skip_stream=True
+ 817skipped_adult_content=skipped_adult_content+1
+ 818self._save_to_file_skipped_streams(stream_channel)
+ 819
+ 820ifnotskip_stream:
+ 821# Some channels have no group,
+ 822# so let's add them to the catch all group
+ 823ifstream_channel["category_id"]isNone:
+ 824stream_channel["category_id"]="9999"
+ 825elifstream_channel["category_id"]!="1":
+ 826pass
+ 827
+ 828# Find the first occurence of the group that the
+ 829# Channel or Stream is pointing to
+ 830the_group=next(
+ 831(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
+ 832None
+ 833)
+ 834
+ 835# Set group title
+ 836ifthe_groupisnotNone:
+ 837group_title=the_group.name
+ 838else:
+ 839ifloading_stream_type==self.live_type:
+ 840group_title=self.live_catch_all_group.name
+ 841the_group=self.live_catch_all_group
+ 842elifloading_stream_type==self.vod_type:
+ 843group_title=self.vod_catch_all_group.name
+ 844the_group=self.vod_catch_all_group
+ 845elifloading_stream_type==self.series_type:
+ 846group_title=self.series_catch_all_group.name
+ 847the_group=self.series_catch_all_group 848
- 849ifloading_stream_type==self.series_type:
- 850# Load all Series
- 851new_series=Serie(self,stream_channel)
- 852# To get all the Episodes for every Season of each
- 853# Series is very time consuming, we will only
- 854# populate the Series once the user click on the
- 855# Series, the Seasons and Episodes will be loaded
- 856# using x.getSeriesInfoByID() function
- 857
- 858else:
- 859new_channel=Channel(
- 860self,
- 861group_title,
- 862stream_channel
- 863)
- 864
- 865ifnew_channel.group_id=="9999":
- 866print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
- 867
- 868# Save the new channel to the local list of channels
- 869ifloading_stream_type==self.live_type:
- 870self.channels.append(new_channel)
- 871elifloading_stream_type==self.vod_type:
- 872self.movies.append(new_channel)
- 873ifnew_channel.age_days_from_added<31:
- 874self.movies_30days.append(new_channel)
- 875ifnew_channel.age_days_from_added<7:
- 876self.movies_7days.append(new_channel)
- 877else:
- 878self.series.append(new_series)
- 879
- 880# Add stream to the specific Group
- 881ifthe_groupisnotNone:
- 882ifloading_stream_type!=self.series_type:
- 883the_group.channels.append(new_channel)
- 884else:
- 885the_group.series.append(new_series)
- 886else:
- 887print(f" - Group not found `{stream_channel['name']}`")
- 888print("\n")
- 889# Print information of which streams have been skipped
- 890ifself.hide_adult_content:
- 891print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
- 892ifskipped_no_name_content>0:
- 893print(f" - Skipped {skipped_no_name_content} unprintable {loading_stream_type} streams")
- 894else:
- 895print(f" - Could not load {loading_stream_type} Streams")
- 896
- 897self.state["loaded"]=True
- 898
- 899def_save_to_file_skipped_streams(self,stream_channel:Channel):
- 900
- 901# Build the full path
- 902full_filename=osp.join(self.cache_path,"skipped_streams.json")
- 903
- 904# If the path makes sense, save the file
- 905json_data=json.dumps(stream_channel,ensure_ascii=False)
- 906try:
- 907withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
- 908myfile.writelines(json_data)
- 909returnTrue
- 910exceptExceptionase:
- 911print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
- 912returnFalse
- 913
- 914defget_series_info_by_id(self,get_series:dict):
- 915"""Get Seasons and Episodes for a Series
- 916
- 917 Args:
- 918 get_series (dict): Series dictionary
- 919 """
- 920
- 921series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
- 922
- 923ifseries_seasons["seasons"]isNone:
- 924series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
- 925
- 926forseries_infoinseries_seasons["seasons"]:
- 927season_name=series_info["name"]
- 928season_key=series_info['season_number']
- 929season=Season(season_name)
- 930get_series.seasons[season_name]=season
- 931if"episodes"inseries_seasons.keys():
- 932forseries_seasoninseries_seasons["episodes"].keys():
- 933forepisode_infoinseries_seasons["episodes"][str(series_season)]:
- 934new_episode_channel=Episode(
- 935self,series_info,"Testing",episode_info
- 936)
- 937season.episodes[episode_info["title"]]=new_episode_channel
- 938
- 939def_get_request(self,url:str,timeout:Tuple=(2,15)):
- 940"""Generic GET Request with Error handling
- 941
- 942 Args:
- 943 URL (str): The URL where to GET content
- 944 timeout (Tuple, optional): Connection and Downloading Timeout. Defaults to (2,15).
- 945
- 946 Returns:
- 947 [type]: JSON dictionary of the loaded data, or None
- 948 """
- 949i=0
- 950whilei<10:
- 951time.sleep(1)
- 952try:
- 953r=requests.get(url,timeout=timeout,headers=self.connection_headers)
- 954i=20
- 955ifr.status_code==200:
- 956returnr.json()
- 957exceptrequests.exceptions.ConnectionError:
- 958print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
- 959i+=1
- 960
- 961exceptrequests.exceptions.HTTPError:
- 962print(" - HTTP Error")
- 963i+=1
- 964
- 965exceptrequests.exceptions.TooManyRedirects:
- 966print(" - TooManyRedirects")
- 967i+=1
- 968
- 969exceptrequests.exceptions.ReadTimeout:
- 970print(" - Timeout while loading data")
- 971i+=1
- 972
- 973returnNone
- 974
- 975# GET Stream Categories
- 976def_load_categories_from_provider(self,stream_type:str):
- 977"""Get from provider all category for specific stream type from provider
- 978
- 979 Args:
- 980 stream_type (str): Stream type can be Live, VOD, Series
- 981
- 982 Returns:
- 983 [type]: JSON if successfull, otherwise None
- 984 """
- 985url=""
- 986ifstream_type==self.live_type:
- 987url=self.get_live_categories_URL()
- 988elifstream_type==self.vod_type:
- 989url=self.get_vod_cat_URL()
- 990elifstream_type==self.series_type:
- 991url=self.get_series_cat_URL()
- 992else:
- 993url=""
- 994
- 995returnself._get_request(url)
- 996
- 997# GET Streams
- 998def_load_streams_from_provider(self,stream_type:str):
- 999"""Get from provider all streams for specific stream type
-1000
-1001 Args:
-1002 stream_type (str): Stream type can be Live, VOD, Series
-1003
-1004 Returns:
-1005 [type]: JSON if successfull, otherwise None
-1006 """
-1007url=""
-1008ifstream_type==self.live_type:
-1009url=self.get_live_streams_URL()
-1010elifstream_type==self.vod_type:
-1011url=self.get_vod_streams_URL()
-1012elifstream_type==self.series_type:
-1013url=self.get_series_URL()
-1014else:
-1015url=""
-1016
-1017returnself._get_request(url)
-1018
-1019# GET Streams by Category
-1020def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
-1021"""Get from provider all streams for specific stream type with category/group ID
-1022
-1023 Args:
-1024 stream_type (str): Stream type can be Live, VOD, Series
-1025 category_id ([type]): Category/Group ID.
-1026
-1027 Returns:
-1028 [type]: JSON if successfull, otherwise None
-1029 """
-1030url=""
-1031
-1032ifstream_type==self.live_type:
-1033url=self.get_live_streams_URL_by_category(category_id)
-1034elifstream_type==self.vod_type:
-1035url=self.get_vod_streams_URL_by_category(category_id)
-1036elifstream_type==self.series_type:
-1037url=self.get_series_URL_by_category(category_id)
-1038else:
-1039url=""
-1040
-1041returnself._get_request(url)
-1042
-1043# GET SERIES Info
-1044def_load_series_info_by_id_from_provider(self,series_id:str):
-1045"""Gets informations about a Serie
-1046
-1047 Args:
-1048 series_id (str): Serie ID as described in Group
-1049
-1050 Returns:
-1051 [type]: JSON if successfull, otherwise None
-1052 """
-1053returnself._get_request(self.get_series_info_URL_by_ID(series_id))
-1054
-1055# The seasons array, might be filled or might be completely empty.
-1056# If it is not empty, it will contain the cover, overview and the air date
-1057# of the selected season.
-1058# In your APP if you want to display the series, you have to take that
-1059# from the episodes array.
-1060
-1061# GET VOD Info
-1062defvodInfoByID(self,vod_id):
-1063returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
-1064
-1065# GET short_epg for LIVE Streams (same as stalker portal,
-1066# prints the next X EPG that will play soon)
-1067defliveEpgByStream(self,stream_id):
-1068returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
-1069
-1070defliveEpgByStreamAndLimit(self,stream_id,limit):
-1071returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
-1072
-1073# GET ALL EPG for LIVE Streams (same as stalker portal,
-1074# but it will print all epg listings regardless of the day)
-1075defallLiveEpgByStream(self,stream_id):
-1076returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
-1077
-1078# Full EPG List for all Streams
-1079defallEpg(self):
-1080returnself._get_request(self.get_all_epg_URL())
-1081
-1082## URL-builder methods
-1083defget_live_categories_URL(self)->str:
-1084returnf"{self.base_url}&action=get_live_categories"
-1085
-1086defget_live_streams_URL(self)->str:
-1087returnf"{self.base_url}&action=get_live_streams"
-1088
-1089defget_live_streams_URL_by_category(self,category_id)->str:
-1090returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
-1091
-1092defget_vod_cat_URL(self)->str:
-1093returnf"{self.base_url}&action=get_vod_categories"
-1094
-1095defget_vod_streams_URL(self)->str:
-1096returnf"{self.base_url}&action=get_vod_streams"
-1097
-1098defget_vod_streams_URL_by_category(self,category_id)->str:
-1099returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
-1100
-1101defget_series_cat_URL(self)->str:
-1102returnf"{self.base_url}&action=get_series_categories"
-1103
-1104defget_series_URL(self)->str:
-1105returnf"{self.base_url}&action=get_series"
-1106
-1107defget_series_URL_by_category(self,category_id)->str:
-1108returnf"{self.base_url}&action=get_series&category_id={category_id}"
-1109
-1110defget_series_info_URL_by_ID(self,series_id)->str:
-1111returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
-1112
-1113defget_VOD_info_URL_by_ID(self,vod_id)->str:
-1114returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
-1115
-1116defget_live_epg_URL_by_stream(self,stream_id)->str:
-1117returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
-1118
-1119defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
-1120returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
-1121
-1122defget_all_live_epg_URL_by_stream(self,stream_id)->str:
-1123returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
-1124
-1125defget_all_epg_URL(self)->str:
-1126returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
+ 849
+ 850ifloading_stream_type==self.series_type:
+ 851# Load all Series
+ 852new_series=Serie(self,stream_channel)
+ 853# To get all the Episodes for every Season of each
+ 854# Series is very time consuming, we will only
+ 855# populate the Series once the user click on the
+ 856# Series, the Seasons and Episodes will be loaded
+ 857# using x.getSeriesInfoByID() function
+ 858
+ 859else:
+ 860new_channel=Channel(
+ 861self,
+ 862group_title,
+ 863stream_channel
+ 864)
+ 865
+ 866ifnew_channel.group_id=="9999":
+ 867print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+ 868
+ 869# Save the new channel to the local list of channels
+ 870ifloading_stream_type==self.live_type:
+ 871self.channels.append(new_channel)
+ 872elifloading_stream_type==self.vod_type:
+ 873self.movies.append(new_channel)
+ 874ifnew_channel.age_days_from_added<31:
+ 875self.movies_30days.append(new_channel)
+ 876ifnew_channel.age_days_from_added<7:
+ 877self.movies_7days.append(new_channel)
+ 878else:
+ 879self.series.append(new_series)
+ 880
+ 881# Add stream to the specific Group
+ 882ifthe_groupisnotNone:
+ 883ifloading_stream_type!=self.series_type:
+ 884the_group.channels.append(new_channel)
+ 885else:
+ 886the_group.series.append(new_series)
+ 887else:
+ 888print(f" - Group not found `{stream_channel['name']}`")
+ 889print("\n")
+ 890# Print information of which streams have been skipped
+ 891ifself.hide_adult_content:
+ 892print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
+ 893ifskipped_no_name_content>0:
+ 894print(f" - Skipped {skipped_no_name_content} unprintable {loading_stream_type} streams")
+ 895else:
+ 896print(f" - Could not load {loading_stream_type} Streams")
+ 897
+ 898self.state["loaded"]=True
+ 899
+ 900def_save_to_file_skipped_streams(self,stream_channel:Channel):
+ 901
+ 902# Build the full path
+ 903full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 904
+ 905# If the path makes sense, save the file
+ 906json_data=json.dumps(stream_channel,ensure_ascii=False)
+ 907try:
+ 908withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
+ 909myfile.writelines(json_data)
+ 910returnTrue
+ 911exceptExceptionase:
+ 912print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
+ 913returnFalse
+ 914
+ 915defget_series_info_by_id(self,get_series:dict):
+ 916"""Get Seasons and Episodes for a Series
+ 917
+ 918 Args:
+ 919 get_series (dict): Series dictionary
+ 920 """
+ 921
+ 922series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
+ 923
+ 924ifseries_seasons["seasons"]isNone:
+ 925series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
+ 926
+ 927forseries_infoinseries_seasons["seasons"]:
+ 928season_name=series_info["name"]
+ 929season_key=series_info['season_number']
+ 930season=Season(season_name)
+ 931get_series.seasons[season_name]=season
+ 932if"episodes"inseries_seasons.keys():
+ 933forseries_seasoninseries_seasons["episodes"].keys():
+ 934forepisode_infoinseries_seasons["episodes"][str(series_season)]:
+ 935new_episode_channel=Episode(
+ 936self,series_info,"Testing",episode_info
+ 937)
+ 938season.episodes[episode_info["title"]]=new_episode_channel
+ 939
+ 940def_get_request(self,url:str,timeout:Tuple=(2,15)):
+ 941"""Generic GET Request with Error handling
+ 942
+ 943 Args:
+ 944 URL (str): The URL where to GET content
+ 945 timeout (Tuple, optional): Connection and Downloading Timeout. Defaults to (2,15).
+ 946
+ 947 Returns:
+ 948 [type]: JSON dictionary of the loaded data, or None
+ 949 """
+ 950i=0
+ 951whilei<10:
+ 952time.sleep(1)
+ 953try:
+ 954r=requests.get(url,timeout=timeout,headers=self.connection_headers)
+ 955i=20
+ 956ifr.status_code==200:
+ 957returnr.json()
+ 958exceptrequests.exceptions.ConnectionError:
+ 959print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
+ 960i+=1
+ 961
+ 962exceptrequests.exceptions.HTTPError:
+ 963print(" - HTTP Error")
+ 964i+=1
+ 965
+ 966exceptrequests.exceptions.TooManyRedirects:
+ 967print(" - TooManyRedirects")
+ 968i+=1
+ 969
+ 970exceptrequests.exceptions.ReadTimeout:
+ 971print(" - Timeout while loading data")
+ 972i+=1
+ 973
+ 974returnNone
+ 975
+ 976# GET Stream Categories
+ 977def_load_categories_from_provider(self,stream_type:str):
+ 978"""Get from provider all category for specific stream type from provider
+ 979
+ 980 Args:
+ 981 stream_type (str): Stream type can be Live, VOD, Series
+ 982
+ 983 Returns:
+ 984 [type]: JSON if successfull, otherwise None
+ 985 """
+ 986url=""
+ 987ifstream_type==self.live_type:
+ 988url=self.get_live_categories_URL()
+ 989elifstream_type==self.vod_type:
+ 990url=self.get_vod_cat_URL()
+ 991elifstream_type==self.series_type:
+ 992url=self.get_series_cat_URL()
+ 993else:
+ 994url=""
+ 995
+ 996returnself._get_request(url)
+ 997
+ 998# GET Streams
+ 999def_load_streams_from_provider(self,stream_type:str):
+1000"""Get from provider all streams for specific stream type
+1001
+1002 Args:
+1003 stream_type (str): Stream type can be Live, VOD, Series
+1004
+1005 Returns:
+1006 [type]: JSON if successfull, otherwise None
+1007 """
+1008url=""
+1009ifstream_type==self.live_type:
+1010url=self.get_live_streams_URL()
+1011elifstream_type==self.vod_type:
+1012url=self.get_vod_streams_URL()
+1013elifstream_type==self.series_type:
+1014url=self.get_series_URL()
+1015else:
+1016url=""
+1017
+1018returnself._get_request(url)
+1019
+1020# GET Streams by Category
+1021def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
+1022"""Get from provider all streams for specific stream type with category/group ID
+1023
+1024 Args:
+1025 stream_type (str): Stream type can be Live, VOD, Series
+1026 category_id ([type]): Category/Group ID.
+1027
+1028 Returns:
+1029 [type]: JSON if successfull, otherwise None
+1030 """
+1031url=""
+1032
+1033ifstream_type==self.live_type:
+1034url=self.get_live_streams_URL_by_category(category_id)
+1035elifstream_type==self.vod_type:
+1036url=self.get_vod_streams_URL_by_category(category_id)
+1037elifstream_type==self.series_type:
+1038url=self.get_series_URL_by_category(category_id)
+1039else:
+1040url=""
+1041
+1042returnself._get_request(url)
+1043
+1044# GET SERIES Info
+1045def_load_series_info_by_id_from_provider(self,series_id:str):
+1046"""Gets informations about a Serie
+1047
+1048 Args:
+1049 series_id (str): Serie ID as described in Group
+1050
+1051 Returns:
+1052 [type]: JSON if successfull, otherwise None
+1053 """
+1054returnself._get_request(self.get_series_info_URL_by_ID(series_id))
+1055
+1056# The seasons array, might be filled or might be completely empty.
+1057# If it is not empty, it will contain the cover, overview and the air date
+1058# of the selected season.
+1059# In your APP if you want to display the series, you have to take that
+1060# from the episodes array.
+1061
+1062# GET VOD Info
+1063defvodInfoByID(self,vod_id):
+1064returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
+1065
+1066# GET short_epg for LIVE Streams (same as stalker portal,
+1067# prints the next X EPG that will play soon)
+1068defliveEpgByStream(self,stream_id):
+1069returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
+1070
+1071defliveEpgByStreamAndLimit(self,stream_id,limit):
+1072returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
+1073
+1074# GET ALL EPG for LIVE Streams (same as stalker portal,
+1075# but it will print all epg listings regardless of the day)
+1076defallLiveEpgByStream(self,stream_id):
+1077returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
+1078
+1079# Full EPG List for all Streams
+1080defallEpg(self):
+1081returnself._get_request(self.get_all_epg_URL())
+1082
+1083## URL-builder methods
+1084defget_live_categories_URL(self)->str:
+1085returnf"{self.base_url}&action=get_live_categories"
+1086
+1087defget_live_streams_URL(self)->str:
+1088returnf"{self.base_url}&action=get_live_streams"
+1089
+1090defget_live_streams_URL_by_category(self,category_id)->str:
+1091returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
+1092
+1093defget_vod_cat_URL(self)->str:
+1094returnf"{self.base_url}&action=get_vod_categories"
+1095
+1096defget_vod_streams_URL(self)->str:
+1097returnf"{self.base_url}&action=get_vod_streams"
+1098
+1099defget_vod_streams_URL_by_category(self,category_id)->str:
+1100returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
+1101
+1102defget_series_cat_URL(self)->str:
+1103returnf"{self.base_url}&action=get_series_categories"
+1104
+1105defget_series_URL(self)->str:
+1106returnf"{self.base_url}&action=get_series"
+1107
+1108defget_series_URL_by_category(self,category_id)->str:
+1109returnf"{self.base_url}&action=get_series&category_id={category_id}"
+1110
+1111defget_series_info_URL_by_ID(self,series_id)->str:
+1112returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
+1113
+1114defget_VOD_info_URL_by_ID(self,vod_id)->str:
+1115returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
+1116
+1117defget_live_epg_URL_by_stream(self,stream_id)->str:
+1118returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
+1119
+1120defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
+1121returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
+1122
+1123defget_all_live_epg_URL_by_stream(self,stream_id)->str:
+1124returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
+1125
+1126defget_all_epg_URL(self)->str:
+1127returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
@@ -3066,547 +3067,548 @@
584r=None 585# Prepare the authentication url 586url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 587whilei<30:
- 588try:
- 589# Request authentication, wait 4 seconds maximum
- 590r=requests.get(url,timeout=(4),headers=self.connection_headers)
- 591i=31
- 592exceptrequests.exceptions.ConnectionError:
- 593time.sleep(1)
- 594print(i)
- 595i+=1
- 596
- 597ifrisnotNone:
- 598# If the answer is ok, process data and change state
- 599ifr.ok:
- 600self.auth_data=r.json()
- 601self.authorization={
- 602"username":self.auth_data["user_info"]["username"],
- 603"password":self.auth_data["user_info"]["password"]
- 604}
- 605# Mark connection authorized
- 606self.state["authenticated"]=True
- 607# Construct the base url for all requests
- 608self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 609# If there is a secure server connection, construct the base url SSL for all requests
- 610if"https_port"inself.auth_data["server_info"]:
- 611self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
- 612f"/player_api.php?username={self.username}&password={self.password}"
- 613else:
- 614print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
- 615else:
- 616print(f"{self.name}: Provider refused the connection")
- 617
- 618def_load_from_file(self,filename)->dict:
- 619"""Try to load the dictionary from file
- 620
- 621 Args:
- 622 filename ([type]): File name containing the data
- 623
- 624 Returns:
- 625 dict: Dictionary if found and no errors, None if file does not exists
- 626 """
- 627# Build the full path
- 628full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 629
- 630# If the cached file exists, attempt to load it
- 631ifosp.isfile(full_filename):
- 632
- 633my_data=None
- 634
- 635# Get the enlapsed seconds since last file update
- 636file_age_sec=time.time()-osp.getmtime(full_filename)
- 637# If the file was updated less than the threshold time,
- 638# it means that the file is still fresh, we can load it.
- 639# Otherwise skip and return None to force a re-download
- 640ifself.threshold_time_sec>file_age_sec:
- 641# Load the JSON data
- 642try:
- 643withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
- 644my_data=json.load(myfile)
- 645iflen(my_data)==0:
- 646my_data=None
- 647exceptExceptionase:
- 648print(f" - Could not load from file `{full_filename}`: e=`{e}`")
- 649returnmy_data
- 650
- 651returnNone
- 652
- 653def_save_to_file(self,data_list:dict,filename:str)->bool:
- 654"""Save a dictionary to file
- 655
- 656 This function will overwrite the file if already exists
- 657
- 658 Args:
- 659 data_list (dict): Dictionary to save
- 660 filename (str): Name of the file
- 661
- 662 Returns:
- 663 bool: True if successfull, False if error
- 664 """
- 665ifdata_listisnotNone:
- 666
- 667#Build the full path
- 668full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 669# If the path makes sense, save the file
- 670json_data=json.dumps(data_list,ensure_ascii=False)
- 671try:
- 672withopen(full_filename,mode="wt",encoding="utf-8")asmyfile:
- 673myfile.write(json_data)
- 674exceptExceptionase:
- 675print(f" - Could not save to file `{full_filename}`: e=`{e}`")
- 676returnFalse
- 677
- 678returnTrue
- 679else:
- 680returnFalse
- 681
- 682defload_iptv(self)->bool:
- 683"""Load XTream IPTV
- 684
- 685 - Add all Live TV to XTream.channels
- 686 - Add all VOD to XTream.movies
- 687 - Add all Series to XTream.series
- 688 Series contains Seasons and Episodes. Those are not automatically
- 689 retrieved from the server to reduce the loading time.
- 690 - Add all groups to XTream.groups
- 691 Groups are for all three channel types, Live TV, VOD, and Series
- 692
- 693 Returns:
- 694 bool: True if successfull, False if error
- 695 """
- 696# If pyxtream has not authenticated the connection, return empty
- 697ifself.state["authenticated"]isFalse:
- 698print("Warning, cannot load steams since authorization failed")
- 699returnFalse
- 700
- 701# If pyxtream has already loaded the data, skip and return success
- 702ifself.state["loaded"]isTrue:
- 703print("Warning, data has already been loaded.")
- 704returnTrue
- 705
- 706forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
- 707## Get GROUPS
- 708
- 709# Try loading local file
- 710dt=0
- 711start=timer()
- 712all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
- 713# If file empty or does not exists, download it from remote
- 714ifall_catisNone:
- 715# Load all Groups and save file locally
- 716all_cat=self._load_categories_from_provider(loading_stream_type)
- 717ifall_catisnotNone:
- 718self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
- 719dt=timer()-start
- 720
- 721# If we got the GROUPS data, show the statistics and load GROUPS
- 722ifall_catisnotNone:
- 723print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
- 724## Add GROUPS to dictionaries
- 725
- 726# Add the catch-all-errors group
- 727ifloading_stream_type==self.live_type:
- 728self.groups.append(self.live_catch_all_group)
- 729elifloading_stream_type==self.vod_type:
- 730self.groups.append(self.vod_catch_all_group)
- 731elifloading_stream_type==self.series_type:
- 732self.groups.append(self.series_catch_all_group)
- 733
- 734forcat_objinall_cat:
- 735ifschemaValidator(cat_obj,SchemaType.GROUP):
- 736# Create Group (Category)
- 737new_group=Group(cat_obj,loading_stream_type)
- 738# Add to xtream class
- 739self.groups.append(new_group)
- 740else:
- 741# Save what did not pass schema validation
- 742print(cat_obj)
- 743
- 744# Sort Categories
- 745self.groups.sort(key=lambdax:x.name)
- 746else:
- 747print(f" - Could not load {loading_stream_type} Groups")
- 748break
- 749
- 750## Get Streams
- 751
- 752# Try loading local file
- 753dt=0
- 754start=timer()
- 755all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
- 756# If file empty or does not exists, download it from remote
- 757ifall_streamsisNone:
- 758# Load all Streams and save file locally
- 759all_streams=self._load_streams_from_provider(loading_stream_type)
- 760self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
- 761dt=timer()-start
- 762
- 763# If we got the STREAMS data, show the statistics and load Streams
- 764ifall_streamsisnotNone:
- 765print(
- 766f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams " \
- 767f"in {dt:.3f} seconds"
- 768)
- 769## Add Streams to dictionaries
- 770
- 771skipped_adult_content=0
- 772skipped_no_name_content=0
- 773
- 774number_of_streams=len(all_streams)
- 775current_stream_number=0
- 776# Calculate 1% of total number of streams
- 777# This is used to slow down the progress bar
- 778one_percent_number_of_streams=number_of_streams/100
- 779start=timer()
- 780forstream_channelinall_streams:
- 781skip_stream=False
- 782current_stream_number+=1
- 783
- 784# Show download progress every 1% of total number of streams
- 785ifcurrent_stream_number<one_percent_number_of_streams:
- 786progress(
- 787current_stream_number,
- 788number_of_streams,
- 789f"Processing {loading_stream_type} Streams"
- 790)
- 791one_percent_number_of_streams*=2
- 792
- 793# Validate JSON scheme
- 794ifself.validate_json:
- 795ifloading_stream_type==self.series_type:
- 796ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
- 797print(stream_channel)
- 798elifloading_stream_type==self.live_type:
- 799ifnotschemaValidator(stream_channel,SchemaType.LIVE):
- 800print(stream_channel)
- 801else:
- 802# vod_type
- 803ifnotschemaValidator(stream_channel,SchemaType.VOD):
- 804print(stream_channel)
- 805
- 806# Skip if the name of the stream is empty
- 807ifstream_channel["name"]=="":
- 808skip_stream=True
- 809skipped_no_name_content=skipped_no_name_content+1
- 810self._save_to_file_skipped_streams(stream_channel)
- 811
- 812# Skip if the user chose to hide adult streams
- 813ifself.hide_adult_contentandloading_stream_type==self.live_type:
- 814if"is_adult"instream_channel:
- 815ifstream_channel["is_adult"]=="1":
- 816skip_stream=True
- 817skipped_adult_content=skipped_adult_content+1
- 818self._save_to_file_skipped_streams(stream_channel)
- 819
- 820ifnotskip_stream:
- 821# Some channels have no group,
- 822# so let's add them to the catch all group
- 823ifstream_channel["category_id"]isNone:
- 824stream_channel["category_id"]="9999"
- 825elifstream_channel["category_id"]!="1":
- 826pass
- 827
- 828# Find the first occurence of the group that the
- 829# Channel or Stream is pointing to
- 830the_group=next(
- 831(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
- 832None
- 833)
- 834
- 835# Set group title
- 836ifthe_groupisnotNone:
- 837group_title=the_group.name
- 838else:
- 839ifloading_stream_type==self.live_type:
- 840group_title=self.live_catch_all_group.name
- 841the_group=self.live_catch_all_group
- 842elifloading_stream_type==self.vod_type:
- 843group_title=self.vod_catch_all_group.name
- 844the_group=self.vod_catch_all_group
- 845elifloading_stream_type==self.series_type:
- 846group_title=self.series_catch_all_group.name
- 847the_group=self.series_catch_all_group
- 848
+ 587print(f"Attempting connection: ",end='')
+ 588whilei<30:
+ 589try:
+ 590# Request authentication, wait 4 seconds maximum
+ 591r=requests.get(url,timeout=(4),headers=self.connection_headers)
+ 592i=31
+ 593exceptrequests.exceptions.ConnectionError:
+ 594time.sleep(1)
+ 595print(f"{i} ",end='',flush=True)
+ 596i+=1
+ 597
+ 598ifrisnotNone:
+ 599# If the answer is ok, process data and change state
+ 600ifr.ok:
+ 601self.auth_data=r.json()
+ 602self.authorization={
+ 603"username":self.auth_data["user_info"]["username"],
+ 604"password":self.auth_data["user_info"]["password"]
+ 605}
+ 606# Mark connection authorized
+ 607self.state["authenticated"]=True
+ 608# Construct the base url for all requests
+ 609self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 610# If there is a secure server connection, construct the base url SSL for all requests
+ 611if"https_port"inself.auth_data["server_info"]:
+ 612self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
+ 613f"/player_api.php?username={self.username}&password={self.password}"
+ 614else:
+ 615print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
+ 616else:
+ 617print(f"\n{self.name}: Provider refused the connection")
+ 618
+ 619def_load_from_file(self,filename)->dict:
+ 620"""Try to load the dictionary from file
+ 621
+ 622 Args:
+ 623 filename ([type]): File name containing the data
+ 624
+ 625 Returns:
+ 626 dict: Dictionary if found and no errors, None if file does not exists
+ 627 """
+ 628# Build the full path
+ 629full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 630
+ 631# If the cached file exists, attempt to load it
+ 632ifosp.isfile(full_filename):
+ 633
+ 634my_data=None
+ 635
+ 636# Get the enlapsed seconds since last file update
+ 637file_age_sec=time.time()-osp.getmtime(full_filename)
+ 638# If the file was updated less than the threshold time,
+ 639# it means that the file is still fresh, we can load it.
+ 640# Otherwise skip and return None to force a re-download
+ 641ifself.threshold_time_sec>file_age_sec:
+ 642# Load the JSON data
+ 643try:
+ 644withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
+ 645my_data=json.load(myfile)
+ 646iflen(my_data)==0:
+ 647my_data=None
+ 648exceptExceptionase:
+ 649print(f" - Could not load from file `{full_filename}`: e=`{e}`")
+ 650returnmy_data
+ 651
+ 652returnNone
+ 653
+ 654def_save_to_file(self,data_list:dict,filename:str)->bool:
+ 655"""Save a dictionary to file
+ 656
+ 657 This function will overwrite the file if already exists
+ 658
+ 659 Args:
+ 660 data_list (dict): Dictionary to save
+ 661 filename (str): Name of the file
+ 662
+ 663 Returns:
+ 664 bool: True if successfull, False if error
+ 665 """
+ 666ifdata_listisnotNone:
+ 667
+ 668#Build the full path
+ 669full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 670# If the path makes sense, save the file
+ 671json_data=json.dumps(data_list,ensure_ascii=False)
+ 672try:
+ 673withopen(full_filename,mode="wt",encoding="utf-8")asmyfile:
+ 674myfile.write(json_data)
+ 675exceptExceptionase:
+ 676print(f" - Could not save to file `{full_filename}`: e=`{e}`")
+ 677returnFalse
+ 678
+ 679returnTrue
+ 680else:
+ 681returnFalse
+ 682
+ 683defload_iptv(self)->bool:
+ 684"""Load XTream IPTV
+ 685
+ 686 - Add all Live TV to XTream.channels
+ 687 - Add all VOD to XTream.movies
+ 688 - Add all Series to XTream.series
+ 689 Series contains Seasons and Episodes. Those are not automatically
+ 690 retrieved from the server to reduce the loading time.
+ 691 - Add all groups to XTream.groups
+ 692 Groups are for all three channel types, Live TV, VOD, and Series
+ 693
+ 694 Returns:
+ 695 bool: True if successfull, False if error
+ 696 """
+ 697# If pyxtream has not authenticated the connection, return empty
+ 698ifself.state["authenticated"]isFalse:
+ 699print("Warning, cannot load steams since authorization failed")
+ 700returnFalse
+ 701
+ 702# If pyxtream has already loaded the data, skip and return success
+ 703ifself.state["loaded"]isTrue:
+ 704print("Warning, data has already been loaded.")
+ 705returnTrue
+ 706
+ 707forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
+ 708## Get GROUPS
+ 709
+ 710# Try loading local file
+ 711dt=0
+ 712start=timer()
+ 713all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
+ 714# If file empty or does not exists, download it from remote
+ 715ifall_catisNone:
+ 716# Load all Groups and save file locally
+ 717all_cat=self._load_categories_from_provider(loading_stream_type)
+ 718ifall_catisnotNone:
+ 719self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
+ 720dt=timer()-start
+ 721
+ 722# If we got the GROUPS data, show the statistics and load GROUPS
+ 723ifall_catisnotNone:
+ 724print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
+ 725## Add GROUPS to dictionaries
+ 726
+ 727# Add the catch-all-errors group
+ 728ifloading_stream_type==self.live_type:
+ 729self.groups.append(self.live_catch_all_group)
+ 730elifloading_stream_type==self.vod_type:
+ 731self.groups.append(self.vod_catch_all_group)
+ 732elifloading_stream_type==self.series_type:
+ 733self.groups.append(self.series_catch_all_group)
+ 734
+ 735forcat_objinall_cat:
+ 736ifschemaValidator(cat_obj,SchemaType.GROUP):
+ 737# Create Group (Category)
+ 738new_group=Group(cat_obj,loading_stream_type)
+ 739# Add to xtream class
+ 740self.groups.append(new_group)
+ 741else:
+ 742# Save what did not pass schema validation
+ 743print(cat_obj)
+ 744
+ 745# Sort Categories
+ 746self.groups.sort(key=lambdax:x.name)
+ 747else:
+ 748print(f" - Could not load {loading_stream_type} Groups")
+ 749break
+ 750
+ 751## Get Streams
+ 752
+ 753# Try loading local file
+ 754dt=0
+ 755start=timer()
+ 756all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
+ 757# If file empty or does not exists, download it from remote
+ 758ifall_streamsisNone:
+ 759# Load all Streams and save file locally
+ 760all_streams=self._load_streams_from_provider(loading_stream_type)
+ 761self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
+ 762dt=timer()-start
+ 763
+ 764# If we got the STREAMS data, show the statistics and load Streams
+ 765ifall_streamsisnotNone:
+ 766print(
+ 767f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams " \
+ 768f"in {dt:.3f} seconds"
+ 769)
+ 770## Add Streams to dictionaries
+ 771
+ 772skipped_adult_content=0
+ 773skipped_no_name_content=0
+ 774
+ 775number_of_streams=len(all_streams)
+ 776current_stream_number=0
+ 777# Calculate 1% of total number of streams
+ 778# This is used to slow down the progress bar
+ 779one_percent_number_of_streams=number_of_streams/100
+ 780start=timer()
+ 781forstream_channelinall_streams:
+ 782skip_stream=False
+ 783current_stream_number+=1
+ 784
+ 785# Show download progress every 1% of total number of streams
+ 786ifcurrent_stream_number<one_percent_number_of_streams:
+ 787progress(
+ 788current_stream_number,
+ 789number_of_streams,
+ 790f"Processing {loading_stream_type} Streams"
+ 791)
+ 792one_percent_number_of_streams*=2
+ 793
+ 794# Validate JSON scheme
+ 795ifself.validate_json:
+ 796ifloading_stream_type==self.series_type:
+ 797ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
+ 798print(stream_channel)
+ 799elifloading_stream_type==self.live_type:
+ 800ifnotschemaValidator(stream_channel,SchemaType.LIVE):
+ 801print(stream_channel)
+ 802else:
+ 803# vod_type
+ 804ifnotschemaValidator(stream_channel,SchemaType.VOD):
+ 805print(stream_channel)
+ 806
+ 807# Skip if the name of the stream is empty
+ 808ifstream_channel["name"]=="":
+ 809skip_stream=True
+ 810skipped_no_name_content=skipped_no_name_content+1
+ 811self._save_to_file_skipped_streams(stream_channel)
+ 812
+ 813# Skip if the user chose to hide adult streams
+ 814ifself.hide_adult_contentandloading_stream_type==self.live_type:
+ 815if"is_adult"instream_channel:
+ 816ifstream_channel["is_adult"]=="1":
+ 817skip_stream=True
+ 818skipped_adult_content=skipped_adult_content+1
+ 819self._save_to_file_skipped_streams(stream_channel)
+ 820
+ 821ifnotskip_stream:
+ 822# Some channels have no group,
+ 823# so let's add them to the catch all group
+ 824ifstream_channel["category_id"]isNone:
+ 825stream_channel["category_id"]="9999"
+ 826elifstream_channel["category_id"]!="1":
+ 827pass
+ 828
+ 829# Find the first occurence of the group that the
+ 830# Channel or Stream is pointing to
+ 831the_group=next(
+ 832(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
+ 833None
+ 834)
+ 835
+ 836# Set group title
+ 837ifthe_groupisnotNone:
+ 838group_title=the_group.name
+ 839else:
+ 840ifloading_stream_type==self.live_type:
+ 841group_title=self.live_catch_all_group.name
+ 842the_group=self.live_catch_all_group
+ 843elifloading_stream_type==self.vod_type:
+ 844group_title=self.vod_catch_all_group.name
+ 845the_group=self.vod_catch_all_group
+ 846elifloading_stream_type==self.series_type:
+ 847group_title=self.series_catch_all_group.name
+ 848the_group=self.series_catch_all_group 849
- 850ifloading_stream_type==self.series_type:
- 851# Load all Series
- 852new_series=Serie(self,stream_channel)
- 853# To get all the Episodes for every Season of each
- 854# Series is very time consuming, we will only
- 855# populate the Series once the user click on the
- 856# Series, the Seasons and Episodes will be loaded
- 857# using x.getSeriesInfoByID() function
- 858
- 859else:
- 860new_channel=Channel(
- 861self,
- 862group_title,
- 863stream_channel
- 864)
- 865
- 866ifnew_channel.group_id=="9999":
- 867print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
- 868
- 869# Save the new channel to the local list of channels
- 870ifloading_stream_type==self.live_type:
- 871self.channels.append(new_channel)
- 872elifloading_stream_type==self.vod_type:
- 873self.movies.append(new_channel)
- 874ifnew_channel.age_days_from_added<31:
- 875self.movies_30days.append(new_channel)
- 876ifnew_channel.age_days_from_added<7:
- 877self.movies_7days.append(new_channel)
- 878else:
- 879self.series.append(new_series)
- 880
- 881# Add stream to the specific Group
- 882ifthe_groupisnotNone:
- 883ifloading_stream_type!=self.series_type:
- 884the_group.channels.append(new_channel)
- 885else:
- 886the_group.series.append(new_series)
- 887else:
- 888print(f" - Group not found `{stream_channel['name']}`")
- 889print("\n")
- 890# Print information of which streams have been skipped
- 891ifself.hide_adult_content:
- 892print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
- 893ifskipped_no_name_content>0:
- 894print(f" - Skipped {skipped_no_name_content} unprintable {loading_stream_type} streams")
- 895else:
- 896print(f" - Could not load {loading_stream_type} Streams")
- 897
- 898self.state["loaded"]=True
- 899
- 900def_save_to_file_skipped_streams(self,stream_channel:Channel):
- 901
- 902# Build the full path
- 903full_filename=osp.join(self.cache_path,"skipped_streams.json")
- 904
- 905# If the path makes sense, save the file
- 906json_data=json.dumps(stream_channel,ensure_ascii=False)
- 907try:
- 908withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
- 909myfile.writelines(json_data)
- 910returnTrue
- 911exceptExceptionase:
- 912print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
- 913returnFalse
- 914
- 915defget_series_info_by_id(self,get_series:dict):
- 916"""Get Seasons and Episodes for a Series
- 917
- 918 Args:
- 919 get_series (dict): Series dictionary
- 920 """
- 921
- 922series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
- 923
- 924ifseries_seasons["seasons"]isNone:
- 925series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
- 926
- 927forseries_infoinseries_seasons["seasons"]:
- 928season_name=series_info["name"]
- 929season_key=series_info['season_number']
- 930season=Season(season_name)
- 931get_series.seasons[season_name]=season
- 932if"episodes"inseries_seasons.keys():
- 933forseries_seasoninseries_seasons["episodes"].keys():
- 934forepisode_infoinseries_seasons["episodes"][str(series_season)]:
- 935new_episode_channel=Episode(
- 936self,series_info,"Testing",episode_info
- 937)
- 938season.episodes[episode_info["title"]]=new_episode_channel
- 939
- 940def_get_request(self,url:str,timeout:Tuple=(2,15)):
- 941"""Generic GET Request with Error handling
- 942
- 943 Args:
- 944 URL (str): The URL where to GET content
- 945 timeout (Tuple, optional): Connection and Downloading Timeout. Defaults to (2,15).
- 946
- 947 Returns:
- 948 [type]: JSON dictionary of the loaded data, or None
- 949 """
- 950i=0
- 951whilei<10:
- 952time.sleep(1)
- 953try:
- 954r=requests.get(url,timeout=timeout,headers=self.connection_headers)
- 955i=20
- 956ifr.status_code==200:
- 957returnr.json()
- 958exceptrequests.exceptions.ConnectionError:
- 959print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
- 960i+=1
- 961
- 962exceptrequests.exceptions.HTTPError:
- 963print(" - HTTP Error")
- 964i+=1
- 965
- 966exceptrequests.exceptions.TooManyRedirects:
- 967print(" - TooManyRedirects")
- 968i+=1
- 969
- 970exceptrequests.exceptions.ReadTimeout:
- 971print(" - Timeout while loading data")
- 972i+=1
- 973
- 974returnNone
- 975
- 976# GET Stream Categories
- 977def_load_categories_from_provider(self,stream_type:str):
- 978"""Get from provider all category for specific stream type from provider
- 979
- 980 Args:
- 981 stream_type (str): Stream type can be Live, VOD, Series
- 982
- 983 Returns:
- 984 [type]: JSON if successfull, otherwise None
- 985 """
- 986url=""
- 987ifstream_type==self.live_type:
- 988url=self.get_live_categories_URL()
- 989elifstream_type==self.vod_type:
- 990url=self.get_vod_cat_URL()
- 991elifstream_type==self.series_type:
- 992url=self.get_series_cat_URL()
- 993else:
- 994url=""
- 995
- 996returnself._get_request(url)
- 997
- 998# GET Streams
- 999def_load_streams_from_provider(self,stream_type:str):
-1000"""Get from provider all streams for specific stream type
-1001
-1002 Args:
-1003 stream_type (str): Stream type can be Live, VOD, Series
-1004
-1005 Returns:
-1006 [type]: JSON if successfull, otherwise None
-1007 """
-1008url=""
-1009ifstream_type==self.live_type:
-1010url=self.get_live_streams_URL()
-1011elifstream_type==self.vod_type:
-1012url=self.get_vod_streams_URL()
-1013elifstream_type==self.series_type:
-1014url=self.get_series_URL()
-1015else:
-1016url=""
-1017
-1018returnself._get_request(url)
-1019
-1020# GET Streams by Category
-1021def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
-1022"""Get from provider all streams for specific stream type with category/group ID
-1023
-1024 Args:
-1025 stream_type (str): Stream type can be Live, VOD, Series
-1026 category_id ([type]): Category/Group ID.
-1027
-1028 Returns:
-1029 [type]: JSON if successfull, otherwise None
-1030 """
-1031url=""
-1032
-1033ifstream_type==self.live_type:
-1034url=self.get_live_streams_URL_by_category(category_id)
-1035elifstream_type==self.vod_type:
-1036url=self.get_vod_streams_URL_by_category(category_id)
-1037elifstream_type==self.series_type:
-1038url=self.get_series_URL_by_category(category_id)
-1039else:
-1040url=""
-1041
-1042returnself._get_request(url)
-1043
-1044# GET SERIES Info
-1045def_load_series_info_by_id_from_provider(self,series_id:str):
-1046"""Gets informations about a Serie
-1047
-1048 Args:
-1049 series_id (str): Serie ID as described in Group
-1050
-1051 Returns:
-1052 [type]: JSON if successfull, otherwise None
-1053 """
-1054returnself._get_request(self.get_series_info_URL_by_ID(series_id))
-1055
-1056# The seasons array, might be filled or might be completely empty.
-1057# If it is not empty, it will contain the cover, overview and the air date
-1058# of the selected season.
-1059# In your APP if you want to display the series, you have to take that
-1060# from the episodes array.
-1061
-1062# GET VOD Info
-1063defvodInfoByID(self,vod_id):
-1064returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
-1065
-1066# GET short_epg for LIVE Streams (same as stalker portal,
-1067# prints the next X EPG that will play soon)
-1068defliveEpgByStream(self,stream_id):
-1069returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
-1070
-1071defliveEpgByStreamAndLimit(self,stream_id,limit):
-1072returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
-1073
-1074# GET ALL EPG for LIVE Streams (same as stalker portal,
-1075# but it will print all epg listings regardless of the day)
-1076defallLiveEpgByStream(self,stream_id):
-1077returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
-1078
-1079# Full EPG List for all Streams
-1080defallEpg(self):
-1081returnself._get_request(self.get_all_epg_URL())
-1082
-1083## URL-builder methods
-1084defget_live_categories_URL(self)->str:
-1085returnf"{self.base_url}&action=get_live_categories"
-1086
-1087defget_live_streams_URL(self)->str:
-1088returnf"{self.base_url}&action=get_live_streams"
-1089
-1090defget_live_streams_URL_by_category(self,category_id)->str:
-1091returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
-1092
-1093defget_vod_cat_URL(self)->str:
-1094returnf"{self.base_url}&action=get_vod_categories"
-1095
-1096defget_vod_streams_URL(self)->str:
-1097returnf"{self.base_url}&action=get_vod_streams"
-1098
-1099defget_vod_streams_URL_by_category(self,category_id)->str:
-1100returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
-1101
-1102defget_series_cat_URL(self)->str:
-1103returnf"{self.base_url}&action=get_series_categories"
-1104
-1105defget_series_URL(self)->str:
-1106returnf"{self.base_url}&action=get_series"
-1107
-1108defget_series_URL_by_category(self,category_id)->str:
-1109returnf"{self.base_url}&action=get_series&category_id={category_id}"
-1110
-1111defget_series_info_URL_by_ID(self,series_id)->str:
-1112returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
-1113
-1114defget_VOD_info_URL_by_ID(self,vod_id)->str:
-1115returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
-1116
-1117defget_live_epg_URL_by_stream(self,stream_id)->str:
-1118returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
-1119
-1120defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
-1121returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
-1122
-1123defget_all_live_epg_URL_by_stream(self,stream_id)->str:
-1124returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
-1125
-1126defget_all_epg_URL(self)->str:
-1127returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
+ 850
+ 851ifloading_stream_type==self.series_type:
+ 852# Load all Series
+ 853new_series=Serie(self,stream_channel)
+ 854# To get all the Episodes for every Season of each
+ 855# Series is very time consuming, we will only
+ 856# populate the Series once the user click on the
+ 857# Series, the Seasons and Episodes will be loaded
+ 858# using x.getSeriesInfoByID() function
+ 859
+ 860else:
+ 861new_channel=Channel(
+ 862self,
+ 863group_title,
+ 864stream_channel
+ 865)
+ 866
+ 867ifnew_channel.group_id=="9999":
+ 868print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+ 869
+ 870# Save the new channel to the local list of channels
+ 871ifloading_stream_type==self.live_type:
+ 872self.channels.append(new_channel)
+ 873elifloading_stream_type==self.vod_type:
+ 874self.movies.append(new_channel)
+ 875ifnew_channel.age_days_from_added<31:
+ 876self.movies_30days.append(new_channel)
+ 877ifnew_channel.age_days_from_added<7:
+ 878self.movies_7days.append(new_channel)
+ 879else:
+ 880self.series.append(new_series)
+ 881
+ 882# Add stream to the specific Group
+ 883ifthe_groupisnotNone:
+ 884ifloading_stream_type!=self.series_type:
+ 885the_group.channels.append(new_channel)
+ 886else:
+ 887the_group.series.append(new_series)
+ 888else:
+ 889print(f" - Group not found `{stream_channel['name']}`")
+ 890print("\n")
+ 891# Print information of which streams have been skipped
+ 892ifself.hide_adult_content:
+ 893print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
+ 894ifskipped_no_name_content>0:
+ 895print(f" - Skipped {skipped_no_name_content} unprintable {loading_stream_type} streams")
+ 896else:
+ 897print(f" - Could not load {loading_stream_type} Streams")
+ 898
+ 899self.state["loaded"]=True
+ 900
+ 901def_save_to_file_skipped_streams(self,stream_channel:Channel):
+ 902
+ 903# Build the full path
+ 904full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 905
+ 906# If the path makes sense, save the file
+ 907json_data=json.dumps(stream_channel,ensure_ascii=False)
+ 908try:
+ 909withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
+ 910myfile.writelines(json_data)
+ 911returnTrue
+ 912exceptExceptionase:
+ 913print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
+ 914returnFalse
+ 915
+ 916defget_series_info_by_id(self,get_series:dict):
+ 917"""Get Seasons and Episodes for a Series
+ 918
+ 919 Args:
+ 920 get_series (dict): Series dictionary
+ 921 """
+ 922
+ 923series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
+ 924
+ 925ifseries_seasons["seasons"]isNone:
+ 926series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
+ 927
+ 928forseries_infoinseries_seasons["seasons"]:
+ 929season_name=series_info["name"]
+ 930season_key=series_info['season_number']
+ 931season=Season(season_name)
+ 932get_series.seasons[season_name]=season
+ 933if"episodes"inseries_seasons.keys():
+ 934forseries_seasoninseries_seasons["episodes"].keys():
+ 935forepisode_infoinseries_seasons["episodes"][str(series_season)]:
+ 936new_episode_channel=Episode(
+ 937self,series_info,"Testing",episode_info
+ 938)
+ 939season.episodes[episode_info["title"]]=new_episode_channel
+ 940
+ 941def_get_request(self,url:str,timeout:Tuple=(2,15)):
+ 942"""Generic GET Request with Error handling
+ 943
+ 944 Args:
+ 945 URL (str): The URL where to GET content
+ 946 timeout (Tuple, optional): Connection and Downloading Timeout. Defaults to (2,15).
+ 947
+ 948 Returns:
+ 949 [type]: JSON dictionary of the loaded data, or None
+ 950 """
+ 951i=0
+ 952whilei<10:
+ 953time.sleep(1)
+ 954try:
+ 955r=requests.get(url,timeout=timeout,headers=self.connection_headers)
+ 956i=20
+ 957ifr.status_code==200:
+ 958returnr.json()
+ 959exceptrequests.exceptions.ConnectionError:
+ 960print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
+ 961i+=1
+ 962
+ 963exceptrequests.exceptions.HTTPError:
+ 964print(" - HTTP Error")
+ 965i+=1
+ 966
+ 967exceptrequests.exceptions.TooManyRedirects:
+ 968print(" - TooManyRedirects")
+ 969i+=1
+ 970
+ 971exceptrequests.exceptions.ReadTimeout:
+ 972print(" - Timeout while loading data")
+ 973i+=1
+ 974
+ 975returnNone
+ 976
+ 977# GET Stream Categories
+ 978def_load_categories_from_provider(self,stream_type:str):
+ 979"""Get from provider all category for specific stream type from provider
+ 980
+ 981 Args:
+ 982 stream_type (str): Stream type can be Live, VOD, Series
+ 983
+ 984 Returns:
+ 985 [type]: JSON if successfull, otherwise None
+ 986 """
+ 987url=""
+ 988ifstream_type==self.live_type:
+ 989url=self.get_live_categories_URL()
+ 990elifstream_type==self.vod_type:
+ 991url=self.get_vod_cat_URL()
+ 992elifstream_type==self.series_type:
+ 993url=self.get_series_cat_URL()
+ 994else:
+ 995url=""
+ 996
+ 997returnself._get_request(url)
+ 998
+ 999# GET Streams
+1000def_load_streams_from_provider(self,stream_type:str):
+1001"""Get from provider all streams for specific stream type
+1002
+1003 Args:
+1004 stream_type (str): Stream type can be Live, VOD, Series
+1005
+1006 Returns:
+1007 [type]: JSON if successfull, otherwise None
+1008 """
+1009url=""
+1010ifstream_type==self.live_type:
+1011url=self.get_live_streams_URL()
+1012elifstream_type==self.vod_type:
+1013url=self.get_vod_streams_URL()
+1014elifstream_type==self.series_type:
+1015url=self.get_series_URL()
+1016else:
+1017url=""
+1018
+1019returnself._get_request(url)
+1020
+1021# GET Streams by Category
+1022def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
+1023"""Get from provider all streams for specific stream type with category/group ID
+1024
+1025 Args:
+1026 stream_type (str): Stream type can be Live, VOD, Series
+1027 category_id ([type]): Category/Group ID.
+1028
+1029 Returns:
+1030 [type]: JSON if successfull, otherwise None
+1031 """
+1032url=""
+1033
+1034ifstream_type==self.live_type:
+1035url=self.get_live_streams_URL_by_category(category_id)
+1036elifstream_type==self.vod_type:
+1037url=self.get_vod_streams_URL_by_category(category_id)
+1038elifstream_type==self.series_type:
+1039url=self.get_series_URL_by_category(category_id)
+1040else:
+1041url=""
+1042
+1043returnself._get_request(url)
+1044
+1045# GET SERIES Info
+1046def_load_series_info_by_id_from_provider(self,series_id:str):
+1047"""Gets informations about a Serie
+1048
+1049 Args:
+1050 series_id (str): Serie ID as described in Group
+1051
+1052 Returns:
+1053 [type]: JSON if successfull, otherwise None
+1054 """
+1055returnself._get_request(self.get_series_info_URL_by_ID(series_id))
+1056
+1057# The seasons array, might be filled or might be completely empty.
+1058# If it is not empty, it will contain the cover, overview and the air date
+1059# of the selected season.
+1060# In your APP if you want to display the series, you have to take that
+1061# from the episodes array.
+1062
+1063# GET VOD Info
+1064defvodInfoByID(self,vod_id):
+1065returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
+1066
+1067# GET short_epg for LIVE Streams (same as stalker portal,
+1068# prints the next X EPG that will play soon)
+1069defliveEpgByStream(self,stream_id):
+1070returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
+1071
+1072defliveEpgByStreamAndLimit(self,stream_id,limit):
+1073returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
+1074
+1075# GET ALL EPG for LIVE Streams (same as stalker portal,
+1076# but it will print all epg listings regardless of the day)
+1077defallLiveEpgByStream(self,stream_id):
+1078returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
+1079
+1080# Full EPG List for all Streams
+1081defallEpg(self):
+1082returnself._get_request(self.get_all_epg_URL())
+1083
+1084## URL-builder methods
+1085defget_live_categories_URL(self)->str:
+1086returnf"{self.base_url}&action=get_live_categories"
+1087
+1088defget_live_streams_URL(self)->str:
+1089returnf"{self.base_url}&action=get_live_streams"
+1090
+1091defget_live_streams_URL_by_category(self,category_id)->str:
+1092returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
+1093
+1094defget_vod_cat_URL(self)->str:
+1095returnf"{self.base_url}&action=get_vod_categories"
+1096
+1097defget_vod_streams_URL(self)->str:
+1098returnf"{self.base_url}&action=get_vod_streams"
+1099
+1100defget_vod_streams_URL_by_category(self,category_id)->str:
+1101returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
+1102
+1103defget_series_cat_URL(self)->str:
+1104returnf"{self.base_url}&action=get_series_categories"
+1105
+1106defget_series_URL(self)->str:
+1107returnf"{self.base_url}&action=get_series"
+1108
+1109defget_series_URL_by_category(self,category_id)->str:
+1110returnf"{self.base_url}&action=get_series&category_id={category_id}"
+1111
+1112defget_series_info_URL_by_ID(self,series_id)->str:
+1113returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
+1114
+1115defget_VOD_info_URL_by_ID(self,vod_id)->str:
+1116returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
+1117
+1118defget_live_epg_URL_by_stream(self,stream_id)->str:
+1119returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
+1120
+1121defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
+1122returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
+1123
+1124defget_all_live_epg_URL_by_stream(self,stream_id)->str:
+1125returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
+1126
+1127defget_all_epg_URL(self)->str:
+1128returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
@@ -4190,36 +4192,37 @@
584r=None585# Prepare the authentication url586url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
-587whilei<30:
-588try:
-589# Request authentication, wait 4 seconds maximum
-590r=requests.get(url,timeout=(4),headers=self.connection_headers)
-591i=31
-592exceptrequests.exceptions.ConnectionError:
-593time.sleep(1)
-594print(i)
-595i+=1
-596
-597ifrisnotNone:
-598# If the answer is ok, process data and change state
-599ifr.ok:
-600self.auth_data=r.json()
-601self.authorization={
-602"username":self.auth_data["user_info"]["username"],
-603"password":self.auth_data["user_info"]["password"]
-604}
-605# Mark connection authorized
-606self.state["authenticated"]=True
-607# Construct the base url for all requests
-608self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
-609# If there is a secure server connection, construct the base url SSL for all requests
-610if"https_port"inself.auth_data["server_info"]:
-611self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
-612f"/player_api.php?username={self.username}&password={self.password}"
-613else:
-614print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
-615else:
-616print(f"{self.name}: Provider refused the connection")
+587print(f"Attempting connection: ",end='')
+588whilei<30:
+589try:
+590# Request authentication, wait 4 seconds maximum
+591r=requests.get(url,timeout=(4),headers=self.connection_headers)
+592i=31
+593exceptrequests.exceptions.ConnectionError:
+594time.sleep(1)
+595print(f"{i} ",end='',flush=True)
+596i+=1
+597
+598ifrisnotNone:
+599# If the answer is ok, process data and change state
+600ifr.ok:
+601self.auth_data=r.json()
+602self.authorization={
+603"username":self.auth_data["user_info"]["username"],
+604"password":self.auth_data["user_info"]["password"]
+605}
+606# Mark connection authorized
+607self.state["authenticated"]=True
+608# Construct the base url for all requests
+609self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+610# If there is a secure server connection, construct the base url SSL for all requests
+611if"https_port"inself.auth_data["server_info"]:
+612self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
+613f"/player_api.php?username={self.username}&password={self.password}"
+614else:
+615print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
+616else:
+617print(f"\n{self.name}: Provider refused the connection")
@@ -4239,223 +4242,223 @@
-
682defload_iptv(self)->bool:
-683"""Load XTream IPTV
-684
-685 - Add all Live TV to XTream.channels
-686 - Add all VOD to XTream.movies
-687 - Add all Series to XTream.series
-688 Series contains Seasons and Episodes. Those are not automatically
-689 retrieved from the server to reduce the loading time.
-690 - Add all groups to XTream.groups
-691 Groups are for all three channel types, Live TV, VOD, and Series
-692
-693 Returns:
-694 bool: True if successfull, False if error
-695 """
-696# If pyxtream has not authenticated the connection, return empty
-697ifself.state["authenticated"]isFalse:
-698print("Warning, cannot load steams since authorization failed")
-699returnFalse
-700
-701# If pyxtream has already loaded the data, skip and return success
-702ifself.state["loaded"]isTrue:
-703print("Warning, data has already been loaded.")
-704returnTrue
-705
-706forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
-707## Get GROUPS
-708
-709# Try loading local file
-710dt=0
-711start=timer()
-712all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
-713# If file empty or does not exists, download it from remote
-714ifall_catisNone:
-715# Load all Groups and save file locally
-716all_cat=self._load_categories_from_provider(loading_stream_type)
-717ifall_catisnotNone:
-718self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
-719dt=timer()-start
-720
-721# If we got the GROUPS data, show the statistics and load GROUPS
-722ifall_catisnotNone:
-723print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
-724## Add GROUPS to dictionaries
-725
-726# Add the catch-all-errors group
-727ifloading_stream_type==self.live_type:
-728self.groups.append(self.live_catch_all_group)
-729elifloading_stream_type==self.vod_type:
-730self.groups.append(self.vod_catch_all_group)
-731elifloading_stream_type==self.series_type:
-732self.groups.append(self.series_catch_all_group)
-733
-734forcat_objinall_cat:
-735ifschemaValidator(cat_obj,SchemaType.GROUP):
-736# Create Group (Category)
-737new_group=Group(cat_obj,loading_stream_type)
-738# Add to xtream class
-739self.groups.append(new_group)
-740else:
-741# Save what did not pass schema validation
-742print(cat_obj)
-743
-744# Sort Categories
-745self.groups.sort(key=lambdax:x.name)
-746else:
-747print(f" - Could not load {loading_stream_type} Groups")
-748break
-749
-750## Get Streams
-751
-752# Try loading local file
-753dt=0
-754start=timer()
-755all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
-756# If file empty or does not exists, download it from remote
-757ifall_streamsisNone:
-758# Load all Streams and save file locally
-759all_streams=self._load_streams_from_provider(loading_stream_type)
-760self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
-761dt=timer()-start
-762
-763# If we got the STREAMS data, show the statistics and load Streams
-764ifall_streamsisnotNone:
-765print(
-766f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams " \
-767f"in {dt:.3f} seconds"
-768)
-769## Add Streams to dictionaries
-770
-771skipped_adult_content=0
-772skipped_no_name_content=0
-773
-774number_of_streams=len(all_streams)
-775current_stream_number=0
-776# Calculate 1% of total number of streams
-777# This is used to slow down the progress bar
-778one_percent_number_of_streams=number_of_streams/100
-779start=timer()
-780forstream_channelinall_streams:
-781skip_stream=False
-782current_stream_number+=1
-783
-784# Show download progress every 1% of total number of streams
-785ifcurrent_stream_number<one_percent_number_of_streams:
-786progress(
-787current_stream_number,
-788number_of_streams,
-789f"Processing {loading_stream_type} Streams"
-790)
-791one_percent_number_of_streams*=2
-792
-793# Validate JSON scheme
-794ifself.validate_json:
-795ifloading_stream_type==self.series_type:
-796ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
-797print(stream_channel)
-798elifloading_stream_type==self.live_type:
-799ifnotschemaValidator(stream_channel,SchemaType.LIVE):
-800print(stream_channel)
-801else:
-802# vod_type
-803ifnotschemaValidator(stream_channel,SchemaType.VOD):
-804print(stream_channel)
-805
-806# Skip if the name of the stream is empty
-807ifstream_channel["name"]=="":
-808skip_stream=True
-809skipped_no_name_content=skipped_no_name_content+1
-810self._save_to_file_skipped_streams(stream_channel)
-811
-812# Skip if the user chose to hide adult streams
-813ifself.hide_adult_contentandloading_stream_type==self.live_type:
-814if"is_adult"instream_channel:
-815ifstream_channel["is_adult"]=="1":
-816skip_stream=True
-817skipped_adult_content=skipped_adult_content+1
-818self._save_to_file_skipped_streams(stream_channel)
-819
-820ifnotskip_stream:
-821# Some channels have no group,
-822# so let's add them to the catch all group
-823ifstream_channel["category_id"]isNone:
-824stream_channel["category_id"]="9999"
-825elifstream_channel["category_id"]!="1":
-826pass
-827
-828# Find the first occurence of the group that the
-829# Channel or Stream is pointing to
-830the_group=next(
-831(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
-832None
-833)
-834
-835# Set group title
-836ifthe_groupisnotNone:
-837group_title=the_group.name
-838else:
-839ifloading_stream_type==self.live_type:
-840group_title=self.live_catch_all_group.name
-841the_group=self.live_catch_all_group
-842elifloading_stream_type==self.vod_type:
-843group_title=self.vod_catch_all_group.name
-844the_group=self.vod_catch_all_group
-845elifloading_stream_type==self.series_type:
-846group_title=self.series_catch_all_group.name
-847the_group=self.series_catch_all_group
-848
+
683defload_iptv(self)->bool:
+684"""Load XTream IPTV
+685
+686 - Add all Live TV to XTream.channels
+687 - Add all VOD to XTream.movies
+688 - Add all Series to XTream.series
+689 Series contains Seasons and Episodes. Those are not automatically
+690 retrieved from the server to reduce the loading time.
+691 - Add all groups to XTream.groups
+692 Groups are for all three channel types, Live TV, VOD, and Series
+693
+694 Returns:
+695 bool: True if successfull, False if error
+696 """
+697# If pyxtream has not authenticated the connection, return empty
+698ifself.state["authenticated"]isFalse:
+699print("Warning, cannot load steams since authorization failed")
+700returnFalse
+701
+702# If pyxtream has already loaded the data, skip and return success
+703ifself.state["loaded"]isTrue:
+704print("Warning, data has already been loaded.")
+705returnTrue
+706
+707forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
+708## Get GROUPS
+709
+710# Try loading local file
+711dt=0
+712start=timer()
+713all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
+714# If file empty or does not exists, download it from remote
+715ifall_catisNone:
+716# Load all Groups and save file locally
+717all_cat=self._load_categories_from_provider(loading_stream_type)
+718ifall_catisnotNone:
+719self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
+720dt=timer()-start
+721
+722# If we got the GROUPS data, show the statistics and load GROUPS
+723ifall_catisnotNone:
+724print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
+725## Add GROUPS to dictionaries
+726
+727# Add the catch-all-errors group
+728ifloading_stream_type==self.live_type:
+729self.groups.append(self.live_catch_all_group)
+730elifloading_stream_type==self.vod_type:
+731self.groups.append(self.vod_catch_all_group)
+732elifloading_stream_type==self.series_type:
+733self.groups.append(self.series_catch_all_group)
+734
+735forcat_objinall_cat:
+736ifschemaValidator(cat_obj,SchemaType.GROUP):
+737# Create Group (Category)
+738new_group=Group(cat_obj,loading_stream_type)
+739# Add to xtream class
+740self.groups.append(new_group)
+741else:
+742# Save what did not pass schema validation
+743print(cat_obj)
+744
+745# Sort Categories
+746self.groups.sort(key=lambdax:x.name)
+747else:
+748print(f" - Could not load {loading_stream_type} Groups")
+749break
+750
+751## Get Streams
+752
+753# Try loading local file
+754dt=0
+755start=timer()
+756all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
+757# If file empty or does not exists, download it from remote
+758ifall_streamsisNone:
+759# Load all Streams and save file locally
+760all_streams=self._load_streams_from_provider(loading_stream_type)
+761self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
+762dt=timer()-start
+763
+764# If we got the STREAMS data, show the statistics and load Streams
+765ifall_streamsisnotNone:
+766print(
+767f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams " \
+768f"in {dt:.3f} seconds"
+769)
+770## Add Streams to dictionaries
+771
+772skipped_adult_content=0
+773skipped_no_name_content=0
+774
+775number_of_streams=len(all_streams)
+776current_stream_number=0
+777# Calculate 1% of total number of streams
+778# This is used to slow down the progress bar
+779one_percent_number_of_streams=number_of_streams/100
+780start=timer()
+781forstream_channelinall_streams:
+782skip_stream=False
+783current_stream_number+=1
+784
+785# Show download progress every 1% of total number of streams
+786ifcurrent_stream_number<one_percent_number_of_streams:
+787progress(
+788current_stream_number,
+789number_of_streams,
+790f"Processing {loading_stream_type} Streams"
+791)
+792one_percent_number_of_streams*=2
+793
+794# Validate JSON scheme
+795ifself.validate_json:
+796ifloading_stream_type==self.series_type:
+797ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
+798print(stream_channel)
+799elifloading_stream_type==self.live_type:
+800ifnotschemaValidator(stream_channel,SchemaType.LIVE):
+801print(stream_channel)
+802else:
+803# vod_type
+804ifnotschemaValidator(stream_channel,SchemaType.VOD):
+805print(stream_channel)
+806
+807# Skip if the name of the stream is empty
+808ifstream_channel["name"]=="":
+809skip_stream=True
+810skipped_no_name_content=skipped_no_name_content+1
+811self._save_to_file_skipped_streams(stream_channel)
+812
+813# Skip if the user chose to hide adult streams
+814ifself.hide_adult_contentandloading_stream_type==self.live_type:
+815if"is_adult"instream_channel:
+816ifstream_channel["is_adult"]=="1":
+817skip_stream=True
+818skipped_adult_content=skipped_adult_content+1
+819self._save_to_file_skipped_streams(stream_channel)
+820
+821ifnotskip_stream:
+822# Some channels have no group,
+823# so let's add them to the catch all group
+824ifstream_channel["category_id"]isNone:
+825stream_channel["category_id"]="9999"
+826elifstream_channel["category_id"]!="1":
+827pass
+828
+829# Find the first occurence of the group that the
+830# Channel or Stream is pointing to
+831the_group=next(
+832(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
+833None
+834)
+835
+836# Set group title
+837ifthe_groupisnotNone:
+838group_title=the_group.name
+839else:
+840ifloading_stream_type==self.live_type:
+841group_title=self.live_catch_all_group.name
+842the_group=self.live_catch_all_group
+843elifloading_stream_type==self.vod_type:
+844group_title=self.vod_catch_all_group.name
+845the_group=self.vod_catch_all_group
+846elifloading_stream_type==self.series_type:
+847group_title=self.series_catch_all_group.name
+848the_group=self.series_catch_all_group849
-850ifloading_stream_type==self.series_type:
-851# Load all Series
-852new_series=Serie(self,stream_channel)
-853# To get all the Episodes for every Season of each
-854# Series is very time consuming, we will only
-855# populate the Series once the user click on the
-856# Series, the Seasons and Episodes will be loaded
-857# using x.getSeriesInfoByID() function
-858
-859else:
-860new_channel=Channel(
-861self,
-862group_title,
-863stream_channel
-864)
-865
-866ifnew_channel.group_id=="9999":
-867print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
-868
-869# Save the new channel to the local list of channels
-870ifloading_stream_type==self.live_type:
-871self.channels.append(new_channel)
-872elifloading_stream_type==self.vod_type:
-873self.movies.append(new_channel)
-874ifnew_channel.age_days_from_added<31:
-875self.movies_30days.append(new_channel)
-876ifnew_channel.age_days_from_added<7:
-877self.movies_7days.append(new_channel)
-878else:
-879self.series.append(new_series)
-880
-881# Add stream to the specific Group
-882ifthe_groupisnotNone:
-883ifloading_stream_type!=self.series_type:
-884the_group.channels.append(new_channel)
-885else:
-886the_group.series.append(new_series)
-887else:
-888print(f" - Group not found `{stream_channel['name']}`")
-889print("\n")
-890# Print information of which streams have been skipped
-891ifself.hide_adult_content:
-892print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
-893ifskipped_no_name_content>0:
-894print(f" - Skipped {skipped_no_name_content} unprintable {loading_stream_type} streams")
-895else:
-896print(f" - Could not load {loading_stream_type} Streams")
-897
-898self.state["loaded"]=True
+850
+851ifloading_stream_type==self.series_type:
+852# Load all Series
+853new_series=Serie(self,stream_channel)
+854# To get all the Episodes for every Season of each
+855# Series is very time consuming, we will only
+856# populate the Series once the user click on the
+857# Series, the Seasons and Episodes will be loaded
+858# using x.getSeriesInfoByID() function
+859
+860else:
+861new_channel=Channel(
+862self,
+863group_title,
+864stream_channel
+865)
+866
+867ifnew_channel.group_id=="9999":
+868print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+869
+870# Save the new channel to the local list of channels
+871ifloading_stream_type==self.live_type:
+872self.channels.append(new_channel)
+873elifloading_stream_type==self.vod_type:
+874self.movies.append(new_channel)
+875ifnew_channel.age_days_from_added<31:
+876self.movies_30days.append(new_channel)
+877ifnew_channel.age_days_from_added<7:
+878self.movies_7days.append(new_channel)
+879else:
+880self.series.append(new_series)
+881
+882# Add stream to the specific Group
+883ifthe_groupisnotNone:
+884ifloading_stream_type!=self.series_type:
+885the_group.channels.append(new_channel)
+886else:
+887the_group.series.append(new_series)
+888else:
+889print(f" - Group not found `{stream_channel['name']}`")
+890print("\n")
+891# Print information of which streams have been skipped
+892ifself.hide_adult_content:
+893print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
+894ifskipped_no_name_content>0:
+895print(f" - Skipped {skipped_no_name_content} unprintable {loading_stream_type} streams")
+896else:
+897print(f" - Could not load {loading_stream_type} Streams")
+898
+899self.state["loaded"]=True
@@ -4488,30 +4491,30 @@
-
915defget_series_info_by_id(self,get_series:dict):
-916"""Get Seasons and Episodes for a Series
-917
-918 Args:
-919 get_series (dict): Series dictionary
-920 """
-921
-922series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
-923
-924ifseries_seasons["seasons"]isNone:
-925series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
-926
-927forseries_infoinseries_seasons["seasons"]:
-928season_name=series_info["name"]
-929season_key=series_info['season_number']
-930season=Season(season_name)
-931get_series.seasons[season_name]=season
-932if"episodes"inseries_seasons.keys():
-933forseries_seasoninseries_seasons["episodes"].keys():
-934forepisode_infoinseries_seasons["episodes"][str(series_season)]:
-935new_episode_channel=Episode(
-936self,series_info,"Testing",episode_info
-937)
-938season.episodes[episode_info["title"]]=new_episode_channel
+
916defget_series_info_by_id(self,get_series:dict):
+917"""Get Seasons and Episodes for a Series
+918
+919 Args:
+920 get_series (dict): Series dictionary
+921 """
+922
+923series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
+924
+925ifseries_seasons["seasons"]isNone:
+926series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
+927
+928forseries_infoinseries_seasons["seasons"]:
+929season_name=series_info["name"]
+930season_key=series_info['season_number']
+931season=Season(season_name)
+932get_series.seasons[season_name]=season
+933if"episodes"inseries_seasons.keys():
+934forseries_seasoninseries_seasons["episodes"].keys():
+935forepisode_infoinseries_seasons["episodes"][str(series_season)]:
+936new_episode_channel=Episode(
+937self,series_info,"Testing",episode_info
+938)
+939season.episodes[episode_info["title"]]=new_episode_channel
From 16d1235dc7820e7ec99c81e66602ff4805507f41 Mon Sep 17 00:00:00 2001
From: Claudio Olmi
Date: Wed, 22 May 2024 00:40:04 -0500
Subject: [PATCH 07/41] Update
---
PYPI.md | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/PYPI.md b/PYPI.md
index 24ecfb7..4baaad5 100644
--- a/PYPI.md
+++ b/PYPI.md
@@ -1,3 +1,7 @@
+# Build docs
+rm -rf doc
+pdoc pyxtream -o doc
+
# Build PIP Module
python3 setup.py sdist bdist_wheel
@@ -7,12 +11,6 @@ twine upload dist/pyxtream-0.7*
# Optional Local Install
python3 -m pip install dist/pyxtream-0.7
-# GitHub Documentation
-
-## Build docs
-rm -rf doc
-pdoc pyxtream
-
# Record TS Video
ffmpeg -y -i "(iptv url)" -c:v copy -c:a copy -map 0:v -map 0:a -t 00:00:30 "myrecording.ts" >"mylog.log" 2>&1
From b8bff9b7084522af74878c26a9527f205f392362 Mon Sep 17 00:00:00 2001
From: Claudio Olmi
Date: Wed, 22 May 2024 13:02:54 -0500
Subject: [PATCH 08/41] Moved doc to docs
---
PYPI.md | 2 +-
{doc => docs}/index.html | 0
{doc => docs}/pyxtream.html | 0
{doc => docs}/pyxtream/progress.html | 0
{doc => docs}/pyxtream/pyxtream.html | 0
{doc => docs}/pyxtream/rest_api.html | 0
{doc => docs}/pyxtream/schemaValidator.html | 0
{doc => docs}/pyxtream/version.html | 0
{doc => docs}/search.js | 0
9 files changed, 1 insertion(+), 1 deletion(-)
rename {doc => docs}/index.html (100%)
rename {doc => docs}/pyxtream.html (100%)
rename {doc => docs}/pyxtream/progress.html (100%)
rename {doc => docs}/pyxtream/pyxtream.html (100%)
rename {doc => docs}/pyxtream/rest_api.html (100%)
rename {doc => docs}/pyxtream/schemaValidator.html (100%)
rename {doc => docs}/pyxtream/version.html (100%)
rename {doc => docs}/search.js (100%)
diff --git a/PYPI.md b/PYPI.md
index 4baaad5..efdd5d0 100644
--- a/PYPI.md
+++ b/PYPI.md
@@ -1,6 +1,6 @@
# Build docs
rm -rf doc
-pdoc pyxtream -o doc
+pdoc pyxtream -o docs
# Build PIP Module
python3 setup.py sdist bdist_wheel
diff --git a/doc/index.html b/docs/index.html
similarity index 100%
rename from doc/index.html
rename to docs/index.html
diff --git a/doc/pyxtream.html b/docs/pyxtream.html
similarity index 100%
rename from doc/pyxtream.html
rename to docs/pyxtream.html
diff --git a/doc/pyxtream/progress.html b/docs/pyxtream/progress.html
similarity index 100%
rename from doc/pyxtream/progress.html
rename to docs/pyxtream/progress.html
diff --git a/doc/pyxtream/pyxtream.html b/docs/pyxtream/pyxtream.html
similarity index 100%
rename from doc/pyxtream/pyxtream.html
rename to docs/pyxtream/pyxtream.html
diff --git a/doc/pyxtream/rest_api.html b/docs/pyxtream/rest_api.html
similarity index 100%
rename from doc/pyxtream/rest_api.html
rename to docs/pyxtream/rest_api.html
diff --git a/doc/pyxtream/schemaValidator.html b/docs/pyxtream/schemaValidator.html
similarity index 100%
rename from doc/pyxtream/schemaValidator.html
rename to docs/pyxtream/schemaValidator.html
diff --git a/doc/pyxtream/version.html b/docs/pyxtream/version.html
similarity index 100%
rename from doc/pyxtream/version.html
rename to docs/pyxtream/version.html
diff --git a/doc/search.js b/docs/search.js
similarity index 100%
rename from doc/search.js
rename to docs/search.js
From 18e3818b0a60f65f6992fbe1e38b3cb0a330a411 Mon Sep 17 00:00:00 2001
From: Claudio Olmi
Date: Mon, 2 Sep 2024 00:17:19 -0500
Subject: [PATCH 09/41] More adjustments
---
.gitignore | 1 +
README.md | 7 +-
docs/pyxtream.html | 4 +-
docs/pyxtream/progress.html | 4 +-
docs/pyxtream/pyxtream.html | 4113 +++++++++++++++-------------
docs/pyxtream/rest_api.html | 419 +--
docs/pyxtream/schemaValidator.html | 10 +-
docs/pyxtream/version.html | 4 +-
docs/search.js | 2 +-
functional_test.py | 47 +-
pyxtream/html/index.html | 246 +-
pyxtream/pyxtream.py | 259 +-
12 files changed, 2769 insertions(+), 2347 deletions(-)
diff --git a/.gitignore b/.gitignore
index b6c4e54..14fa750 100644
--- a/.gitignore
+++ b/.gitignore
@@ -85,6 +85,7 @@ ipython_config.py
# pyenv
.python-version
+myenv
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
diff --git a/README.md b/README.md
index b869167..598626a 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,11 @@ python3 functional_test.py
The functional test will allow you to authenticate on startup, load and search streams. If Flask is installed, a simple website will be available at http://localhost:5000 to allow you to search and play streams.
-### Interesting Work by somebody else
+## Interesting Work by somebody else
+
+- xtreamPOC - Project is a Proof of Concept (POC) that leverages pyxtream, MPV, and NiceGUI to demonstrate the use of Xtream Portal Codes.
+
+##
So far there is no ready to use Transport Stream library for playing live stream.
@@ -112,6 +116,7 @@ xTream.movies[{},{},...]
| Date | Version | Description |
| ----------- | -----| ----------- |
+| 2024-09-02 | 0.7.2 | - - - |
| 2024-05-21 | 0.7.1 | - Fixed missing jsonschema package - Fixed provider name in functional_test - Improved print out of connection attempts - Added method to read latest changes in functional_test
| 2023-11-08 | 0.7.0 | - Added Schema Validator - Added Channel Age - Added list of movies added in the last 30 and 7 days - Updated code based on PyLint - Fixed Flask package to be optional [richard-de-vos](https://github.com/richard-de-vos)|
| 2023-02-06 | 0.6.0 | - Added methods to change connection header, to turn off reload timer, and to enable/disable Flask debug mode - Added a loop when attempting to connect to the provider - Cleaned up some print lines|
diff --git a/docs/pyxtream.html b/docs/pyxtream.html
index 3c2fc21..135d34b 100644
--- a/docs/pyxtream.html
+++ b/docs/pyxtream.html
@@ -3,14 +3,14 @@
-
+
pyxtream API documentation
-
+
24fromosimportremove 25# Timing xtream json downloads 26fromtimeitimportdefault_timerastimer
- 27fromtypingimportList,Tuple
- 28fromdatetimeimportdatetime
+ 27fromtypingimportList,Tuple,Optional
+ 28fromdatetimeimportdatetime,timedelta 29importrequests 30 31frompyxtream.schemaValidatorimportSchemaType,schemaValidator
@@ -781,789 +781,870 @@
342 provider_url (str): URL of the IPTV provider 343 headers (dict): Requests Headers 344 hide_adult_content(bool, optional): When `True` hide stream that are marked for adult
- 345 cache_path (str, optional): Location where to save loaded files. Defaults to empty string.
- 346 reload_time_sec (int, optional): Number of seconds before automatic reloading (-1 to turn it OFF)
- 347 debug_flask (bool, optional): Enable the debug mode in Flask
- 348 validate_json (bool, optional): Check Xtream API provided JSON for validity
- 349
- 350 Returns: XTream Class Instance
+ 345 cache_path (str, optional): Location where to save loaded files.
+ 346 Defaults to empty string.
+ 347 reload_time_sec (int, optional): Number of seconds before automatic reloading
+ 348 (-1 to turn it OFF)
+ 349 debug_flask (bool, optional): Enable the debug mode in Flask
+ 350 validate_json (bool, optional): Check Xtream API provided JSON for validity 351
- 352 - Note 1: If it fails to authorize with provided username and password,
- 353 auth_data will be an empty dictionary.
- 354 - Note 2: The JSON validation option will take considerable amount of time and it should be
- 355 used only as a debug tool. The Xtream API JSON from the provider passes through a schema
- 356 that represent the best available understanding of how the Xtream API works.
- 357 """
- 358self.server=provider_url
- 359self.username=provider_username
- 360self.password=provider_password
- 361self.name=provider_name
- 362self.cache_path=cache_path
- 363self.hide_adult_content=hide_adult_content
- 364self.threshold_time_sec=reload_time_sec
- 365self.validate_json=validate_json
- 366
- 367# get the pyxtream local path
- 368self.app_fullpath=osp.dirname(osp.realpath(__file__))
+ 352 Returns: XTream Class Instance
+ 353
+ 354 - Note 1: If it fails to authorize with provided username and password,
+ 355 auth_data will be an empty dictionary.
+ 356 - Note 2: The JSON validation option will take considerable amount of time and it should be
+ 357 used only as a debug tool. The Xtream API JSON from the provider passes through a
+ 358 schema that represent the best available understanding of how the Xtream API
+ 359 works.
+ 360 """
+ 361self.server=provider_url
+ 362self.username=provider_username
+ 363self.password=provider_password
+ 364self.name=provider_name
+ 365self.cache_path=cache_path
+ 366self.hide_adult_content=hide_adult_content
+ 367self.threshold_time_sec=reload_time_sec
+ 368self.validate_json=validate_json 369
- 370# prepare location of local html template
- 371self.html_template_folder=osp.join(self.app_fullpath,"html")
+ 370# get the pyxtream local path
+ 371self.app_fullpath=osp.dirname(osp.realpath(__file__)) 372
- 373# if the cache_path is specified, test that it is a directory
- 374ifself.cache_path!="":
- 375# If the cache_path is not a directory, clear it
- 376ifnotosp.isdir(self.cache_path):
- 377print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
- 378self.cache_path==""
- 379
- 380# If the cache_path is still empty, use default
- 381ifself.cache_path=="":
- 382self.cache_path=osp.expanduser("~/.xtream-cache/")
- 383ifnotosp.isdir(self.cache_path):
- 384makedirs(self.cache_path,exist_ok=True)
- 385print(f"pyxtream cache path located at {self.cache_path}")
- 386
- 387ifheadersisnotNone:
- 388self.connection_headers=headers
- 389else:
- 390self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
- 391
- 392self.authenticate()
- 393
- 394ifself.threshold_time_sec>0:
- 395print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
- 396else:
- 397print("Reload timer is OFF")
- 398
- 399ifself.state['authenticated']:
- 400ifUSE_FLASK:
- 401self.flaskapp=FlaskWrap('pyxtream',self,self.html_template_folder,debug=debug_flask)
- 402self.flaskapp.start()
- 403
- 404defsearch_stream(self,keyword:str,ignore_case:bool=True,return_type:str="LIST")->List:
- 405"""Search for streams
+ 373# prepare location of local html template
+ 374self.html_template_folder=osp.join(self.app_fullpath,"html")
+ 375
+ 376# if the cache_path is specified, test that it is a directory
+ 377ifself.cache_path!="":
+ 378# If the cache_path is not a directory, clear it
+ 379ifnotosp.isdir(self.cache_path):
+ 380print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
+ 381self.cache_path==""
+ 382
+ 383# If the cache_path is still empty, use default
+ 384ifself.cache_path=="":
+ 385self.cache_path=osp.expanduser("~/.xtream-cache/")
+ 386ifnotosp.isdir(self.cache_path):
+ 387makedirs(self.cache_path,exist_ok=True)
+ 388print(f"pyxtream cache path located at {self.cache_path}")
+ 389
+ 390ifheadersisnotNone:
+ 391self.connection_headers=headers
+ 392else:
+ 393self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
+ 394
+ 395self.authenticate()
+ 396
+ 397ifself.threshold_time_sec>0:
+ 398print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
+ 399else:
+ 400print("Reload timer is OFF")
+ 401
+ 402ifself.state['authenticated']:
+ 403ifUSE_FLASK:
+ 404self.flaskapp=FlaskWrap('pyxtream',self,self.html_template_folder,debug=debug_flask)
+ 405self.flaskapp.start() 406
- 407 Args:
- 408 keyword (str): Keyword to search for. Supports REGEX
- 409 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
- 410 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
- 411
- 412 Returns:
- 413 List: List with all the results, it could be empty. Each result
- 414 """
- 415
- 416search_result=[]
- 417
- 418ifignore_case:
- 419regex=re.compile(keyword,re.IGNORECASE)
- 420else:
- 421regex=re.compile(keyword)
+ 407defsearch_stream(self,keyword:str,
+ 408ignore_case:bool=True,
+ 409return_type:str="LIST",
+ 410stream_type:list=("series","movies","channels"))->list:
+ 411"""Search for streams
+ 412
+ 413 Args:
+ 414 keyword (str): Keyword to search for. Supports REGEX
+ 415 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
+ 416 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
+ 417 stream_type (list, optional): Search within specific stream type.
+ 418
+ 419 Returns:
+ 420 list: List with all the results, it could be empty.
+ 421 """ 422
- 423print(f"Checking {len(self.movies)} movies")
- 424forstreaminself.movies:
- 425ifre.match(regex,stream.name)isnotNone:
- 426search_result.append(stream.export_json())
- 427
- 428print(f"Checking {len(self.channels)} channels")
- 429forstreaminself.channels:
- 430ifre.match(regex,stream.name)isnotNone:
- 431search_result.append(stream.export_json())
- 432
- 433print(f"Checking {len(self.series)} series")
- 434forstreaminself.series:
- 435ifre.match(regex,stream.name)isnotNone:
- 436search_result.append(stream.export_json())
- 437
- 438ifreturn_type=="JSON":
- 439ifsearch_resultisnotNone:
- 440print(f"Found {len(search_result)} results `{keyword}`")
- 441returnjson.dumps(search_result,ensure_ascii=False)
+ 423search_result=[]
+ 424regex_flags=re.IGNORECASEifignore_caseelse0
+ 425regex=re.compile(keyword,regex_flags)
+ 426# if ignore_case:
+ 427# regex = re.compile(keyword, re.IGNORECASE)
+ 428# else:
+ 429# regex = re.compile(keyword)
+ 430
+ 431# if "movies" in stream_type:
+ 432# print(f"Checking {len(self.movies)} movies")
+ 433# for stream in self.movies:
+ 434# if re.match(regex, stream.name) is not None:
+ 435# search_result.append(stream.export_json())
+ 436
+ 437# if "channels" in stream_type:
+ 438# print(f"Checking {len(self.channels)} channels")
+ 439# for stream in self.channels:
+ 440# if re.match(regex, stream.name) is not None:
+ 441# search_result.append(stream.export_json()) 442
- 443returnsearch_result
- 444
- 445defdownload_video(self,stream_id:int)->str:
- 446"""Download Video from Stream ID
- 447
- 448 Args:
- 449 stream_id (int): Stirng identifing the stream ID
- 450
- 451 Returns:
- 452 str: Absolute Path Filename where the file was saved. Empty if could not download
- 453 """
- 454url=""
- 455filename=""
- 456forstreaminself.movies:
- 457ifstream.id==stream_id:
- 458url=stream.url
- 459fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
- 460filename=osp.join(self.cache_path,fn)
- 461
- 462# If the url was correctly built and file does not exists, start downloading
- 463ifurl!="":
- 464ifnotosp.isfile(filename):
- 465ifnotself._download_video_impl(url,filename):
- 466return"Error"
- 467
- 468returnfilename
+ 443# if "series" in stream_type:
+ 444# print(f"Checking {len(self.series)} series")
+ 445# for stream in self.series:
+ 446# if re.match(regex, stream.name) is not None:
+ 447# search_result.append(stream.export_json())
+ 448
+ 449stream_collections={
+ 450"movies":self.movies,
+ 451"channels":self.channels,
+ 452"series":self.series
+ 453}
+ 454
+ 455forstream_type_nameinstream_type:
+ 456ifstream_type_nameinstream_collections:
+ 457collection=stream_collections[stream_type_name]
+ 458print(f"Checking {len(collection)}{stream_type_name}")
+ 459forstreamincollection:
+ 460ifre.match(regex,stream.name)isnotNone:
+ 461search_result.append(stream.export_json())
+ 462else:
+ 463print(f"`{stream_type_name}` not found in collection")
+ 464
+ 465ifreturn_type=="JSON":
+ 466# if search_result is not None:
+ 467print(f"Found {len(search_result)} results `{keyword}`")
+ 468returnjson.dumps(search_result,ensure_ascii=False) 469
- 470def_download_video_impl(self,url:str,fullpath_filename:str)->bool:
- 471"""Download a stream
- 472
- 473 Args:
- 474 url (str): Complete URL of the stream
- 475 fullpath_filename (str): Complete File path where to save the stream
- 476
- 477 Returns:
- 478 bool: True if successful, False if error
- 479 """
- 480ret_code=False
- 481mb_size=1024*1024
- 482try:
- 483print(f"Downloading from URL `{url}` and saving at `{fullpath_filename}`")
- 484
- 485# Make the request to download
- 486response=requests.get(url,timeout=(5),stream=True,allow_redirects=True,headers=self.connection_headers)
- 487# If there is an answer from the remote server
- 488ifresponse.status_code==200:
- 489# Get content type Binary or Text
- 490content_type=response.headers.get('content-type',None)
- 491
- 492# Get total playlist byte size
- 493total_content_size=int(response.headers.get('content-length',None))
- 494total_content_size_mb=total_content_size/mb_size
- 495
- 496# Set downloaded size
- 497downloaded_bytes=0
- 498
- 499# Set stream blocks
- 500block_bytes=int(4*mb_size)# 4 MB
- 501
- 502print(f"Ready to download {total_content_size_mb:.1f} MB file ({total_content_size})")
- 503ifcontent_type.split('/')[0]!="text":
- 504withopen(fullpath_filename,"wb")asfile:
- 505
- 506# Grab data by block_bytes
- 507fordatainresponse.iter_content(block_bytes,decode_unicode=False):
- 508downloaded_bytes+=block_bytes
- 509progress(downloaded_bytes,total_content_size,"Downloading")
- 510file.write(data)
- 511ifdownloaded_bytes<total_content_size:
- 512print("The file size is incorrect, deleting")
- 513remove(fullpath_filename)
- 514else:
- 515# Set the datatime when it was last retreived
- 516# self.settings.set_
- 517print("")
- 518ret_code=True
- 519else:
- 520print(f"URL has a file with unexpected content-type {content_type}")
- 521else:
- 522print(f"HTTP error {response.status_code} while retrieving from {url}")
- 523exceptExceptionase:
- 524print(e)
- 525
- 526returnret_code
- 527
- 528def_slugify(self,string:str)->str:
- 529"""Normalize string
- 530
- 531 Normalizes string, converts to lowercase, removes non-alpha characters,
- 532 and converts spaces to hyphens.
+ 470returnsearch_result
+ 471
+ 472defdownload_video(self,stream_id:int)->str:
+ 473"""Download Video from Stream ID
+ 474
+ 475 Args:
+ 476 stream_id (int): Stirng identifing the stream ID
+ 477
+ 478 Returns:
+ 479 str: Absolute Path Filename where the file was saved. Empty if could not download
+ 480 """
+ 481url=""
+ 482filename=""
+ 483forstreaminself.movies:
+ 484ifstream.id==stream_id:
+ 485url=stream.url
+ 486fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
+ 487filename=osp.join(self.cache_path,fn)
+ 488
+ 489# If the url was correctly built and file does not exists, start downloading
+ 490ifurl!="":
+ 491#if not osp.isfile(filename):
+ 492ifnotself._download_video_impl(url,filename):
+ 493return"Error"
+ 494
+ 495returnfilename
+ 496
+ 497def_download_video_impl(self,url:str,fullpath_filename:str)->bool:
+ 498"""Download a stream
+ 499
+ 500 Args:
+ 501 url (str): Complete URL of the stream
+ 502 fullpath_filename (str): Complete File path where to save the stream
+ 503
+ 504 Returns:
+ 505 bool: True if successful, False if error
+ 506 """
+ 507ret_code=False
+ 508mb_size=1024*1024
+ 509try:
+ 510print(f"Downloading from URL `{url}` and saving at `{fullpath_filename}`")
+ 511
+ 512# Check if the file already exists
+ 513ifosp.exists(fullpath_filename):
+ 514# If the file exists, resume the download from where it left off
+ 515file_size=osp.getsize(fullpath_filename)
+ 516self.connection_headers['Range']=f'bytes={file_size}-'
+ 517mode='ab'# Append to the existing file
+ 518print(f"Resuming from {file_size:_} bytes")
+ 519else:
+ 520# If the file does not exist, start a new download
+ 521mode='wb'# Write a new file
+ 522
+ 523# Make the request to download
+ 524response=requests.get(url,timeout=(10),stream=True,allow_redirects=True,headers=self.connection_headers)
+ 525# If there is an answer from the remote server
+ 526ifresponse.status_code==200orresponse.status_code==206:
+ 527# Get content type Binary or Text
+ 528content_type=response.headers.get('content-type',None)
+ 529
+ 530# Get total playlist byte size
+ 531total_content_size=int(response.headers.get('content-length',None))
+ 532total_content_size_mb=total_content_size/mb_size 533
- 534 Args:
- 535 string (str): String to be normalized
+ 534# Set downloaded size
+ 535downloaded_bytes=0 536
- 537 Returns:
- 538 str: Normalized String
- 539 """
- 540return"".join(x.lower()forxinstringifx.isprintable())
- 541
- 542def_validate_url(self,url:str)->bool:
- 543regex=re.compile(
- 544r"^(?:http|ftp)s?://"# http:// or https://
- 545r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"# domain...
- 546r"localhost|"# localhost...
- 547r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"# ...or ip
- 548r"(?::\d+)?"# optional port
- 549r"(?:/?|[/?]\S+)$",
- 550re.IGNORECASE,
- 551)
+ 537# Set stream blocks
+ 538block_bytes=int(4*mb_size)# 4 MB
+ 539
+ 540print(f"Ready to download {total_content_size_mb:.1f} MB file ({total_content_size})")
+ 541ifcontent_type.split('/')[0]!="text":
+ 542withopen(fullpath_filename,mode)asfile:
+ 543
+ 544# Grab data by block_bytes
+ 545fordatainresponse.iter_content(block_bytes,decode_unicode=False):
+ 546downloaded_bytes+=block_bytes
+ 547progress(downloaded_bytes,total_content_size,"Downloading")
+ 548file.write(data)
+ 549
+ 550ifdownloaded_bytes==total_content_size:
+ 551ret_code=True 552
- 553returnre.match(regex,url)isnotNone
- 554
- 555def_get_logo_local_path(self,logo_url:str)->str:
- 556"""Convert the Logo URL to a local Logo Path
- 557
- 558 Args:
- 559 logoURL (str): The Logo URL
- 560
- 561 Returns:
- 562 [type]: The logo path as a string or None
- 563 """
- 564local_logo_path=None
- 565iflogo_urlisnotNone:
- 566ifnotself._validate_url(logo_url):
- 567logo_url=None
- 568else:
- 569local_logo_path=osp.join(
- 570self.cache_path,
- 571f"{self._slugify(self.name)}-{self._slugify(osp.split(logo_url)[-1])}"
- 572)
- 573returnlocal_logo_path
- 574
- 575defauthenticate(self):
- 576"""Login to provider"""
- 577# If we have not yet successfully authenticated, attempt authentication
- 578ifself.state["authenticated"]isFalse:
- 579# Erase any previous data
- 580self.auth_data={}
- 581# Loop through 30 seconds
- 582i=0
- 583r=None
- 584# Prepare the authentication url
- 585url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 586print(f"Attempting connection: ",end='')
- 587whilei<30:
- 588try:
- 589# Request authentication, wait 4 seconds maximum
- 590r=requests.get(url,timeout=(4),headers=self.connection_headers)
- 591i=31
- 592exceptrequests.exceptions.ConnectionError:
- 593time.sleep(1)
- 594print(f"{i} ",end='',flush=True)
- 595i+=1
+ 553# Delete Range if it was added
+ 554try:
+ 555delself.connection_headers['Range']
+ 556exceptKeyError:
+ 557pass
+ 558else:
+ 559print(f"URL has a file with unexpected content-type {content_type}")
+ 560else:
+ 561print(f"HTTP error {response.status_code} while retrieving from {url}")
+ 562exceptExceptionase:
+ 563print(e)
+ 564
+ 565returnret_code
+ 566
+ 567def_slugify(self,string:str)->str:
+ 568"""Normalize string
+ 569
+ 570 Normalizes string, converts to lowercase, removes non-alpha characters,
+ 571 and converts spaces to hyphens.
+ 572
+ 573 Args:
+ 574 string (str): String to be normalized
+ 575
+ 576 Returns:
+ 577 str: Normalized String
+ 578 """
+ 579return"".join(x.lower()forxinstringifx.isprintable())
+ 580
+ 581def_validate_url(self,url:str)->bool:
+ 582regex=re.compile(
+ 583r"^(?:http|ftp)s?://"# http:// or https://
+ 584r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"# domain...
+ 585r"localhost|"# localhost...
+ 586r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"# ...or ip
+ 587r"(?::\d+)?"# optional port
+ 588r"(?:/?|[/?]\S+)$",
+ 589re.IGNORECASE,
+ 590)
+ 591
+ 592returnre.match(regex,url)isnotNone
+ 593
+ 594def_get_logo_local_path(self,logo_url:str)->str:
+ 595"""Convert the Logo URL to a local Logo Path 596
- 597ifrisnotNone:
- 598# If the answer is ok, process data and change state
- 599ifr.ok:
- 600self.auth_data=r.json()
- 601self.authorization={
- 602"username":self.auth_data["user_info"]["username"],
- 603"password":self.auth_data["user_info"]["password"]
- 604}
- 605# Mark connection authorized
- 606self.state["authenticated"]=True
- 607# Construct the base url for all requests
- 608self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 609# If there is a secure server connection, construct the base url SSL for all requests
- 610if"https_port"inself.auth_data["server_info"]:
- 611self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
- 612f"/player_api.php?username={self.username}&password={self.password}"
- 613else:
- 614print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
- 615else:
- 616print(f"\n{self.name}: Provider refused the connection")
- 617
- 618def_load_from_file(self,filename)->dict:
- 619"""Try to load the dictionary from file
- 620
- 621 Args:
- 622 filename ([type]): File name containing the data
- 623
- 624 Returns:
- 625 dict: Dictionary if found and no errors, None if file does not exists
- 626 """
- 627# Build the full path
- 628full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 629
- 630# If the cached file exists, attempt to load it
- 631ifosp.isfile(full_filename):
- 632
- 633my_data=None
- 634
- 635# Get the enlapsed seconds since last file update
- 636file_age_sec=time.time()-osp.getmtime(full_filename)
- 637# If the file was updated less than the threshold time,
- 638# it means that the file is still fresh, we can load it.
- 639# Otherwise skip and return None to force a re-download
- 640ifself.threshold_time_sec>file_age_sec:
- 641# Load the JSON data
- 642try:
- 643withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
- 644my_data=json.load(myfile)
- 645iflen(my_data)==0:
- 646my_data=None
- 647exceptExceptionase:
- 648print(f" - Could not load from file `{full_filename}`: e=`{e}`")
- 649returnmy_data
- 650
- 651returnNone
- 652
- 653def_save_to_file(self,data_list:dict,filename:str)->bool:
- 654"""Save a dictionary to file
- 655
- 656 This function will overwrite the file if already exists
- 657
- 658 Args:
- 659 data_list (dict): Dictionary to save
- 660 filename (str): Name of the file
- 661
- 662 Returns:
- 663 bool: True if successfull, False if error
- 664 """
- 665ifdata_listisnotNone:
- 666
- 667#Build the full path
- 668full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 669# If the path makes sense, save the file
- 670json_data=json.dumps(data_list,ensure_ascii=False)
- 671try:
- 672withopen(full_filename,mode="wt",encoding="utf-8")asmyfile:
- 673myfile.write(json_data)
- 674exceptExceptionase:
- 675print(f" - Could not save to file `{full_filename}`: e=`{e}`")
- 676returnFalse
- 677
- 678returnTrue
- 679else:
- 680returnFalse
+ 597 Args:
+ 598 logoURL (str): The Logo URL
+ 599
+ 600 Returns:
+ 601 [type]: The logo path as a string or None
+ 602 """
+ 603local_logo_path=None
+ 604iflogo_urlisnotNone:
+ 605ifnotself._validate_url(logo_url):
+ 606logo_url=None
+ 607else:
+ 608local_logo_path=osp.join(
+ 609self.cache_path,
+ 610f"{self._slugify(self.name)}-{self._slugify(osp.split(logo_url)[-1])}"
+ 611)
+ 612returnlocal_logo_path
+ 613
+ 614defauthenticate(self):
+ 615"""Login to provider"""
+ 616# If we have not yet successfully authenticated, attempt authentication
+ 617ifself.state["authenticated"]isFalse:
+ 618# Erase any previous data
+ 619self.auth_data={}
+ 620# Loop through 30 seconds
+ 621i=0
+ 622r=None
+ 623# Prepare the authentication url
+ 624url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 625print("Attempting connection... ",end='')
+ 626whilei<30:
+ 627try:
+ 628# Request authentication, wait 4 seconds maximum
+ 629r=requests.get(url,timeout=(4),headers=self.connection_headers)
+ 630i=31
+ 631exceptrequests.exceptions.ConnectionError:
+ 632time.sleep(1)
+ 633print(f"{i} ",end='',flush=True)
+ 634i+=1
+ 635
+ 636ifrisnotNone:
+ 637# If the answer is ok, process data and change state
+ 638ifr.ok:
+ 639print("Connected")
+ 640self.auth_data=r.json()
+ 641self.authorization={
+ 642"username":self.auth_data["user_info"]["username"],
+ 643"password":self.auth_data["user_info"]["password"]
+ 644}
+ 645# Account expiration date
+ 646self.account_expiration=timedelta(
+ 647seconds=(
+ 648int(self.auth_data["user_info"]["exp_date"])-datetime.now().timestamp()
+ 649)
+ 650)
+ 651# Mark connection authorized
+ 652self.state["authenticated"]=True
+ 653# Construct the base url for all requests
+ 654self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 655# If there is a secure server connection, construct the base url SSL for all requests
+ 656if"https_port"inself.auth_data["server_info"]:
+ 657self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
+ 658f"/player_api.php?username={self.username}&password={self.password}"
+ 659print(f"Account expires in {str(self.account_expiration)}")
+ 660else:
+ 661print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
+ 662else:
+ 663print(f"\n{self.name}: Provider refused the connection")
+ 664
+ 665def_load_from_file(self,filename)->dict:
+ 666"""Try to load the dictionary from file
+ 667
+ 668 Args:
+ 669 filename ([type]): File name containing the data
+ 670
+ 671 Returns:
+ 672 dict: Dictionary if found and no errors, None if file does not exists
+ 673 """
+ 674# Build the full path
+ 675full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 676
+ 677# If the cached file exists, attempt to load it
+ 678ifosp.isfile(full_filename):
+ 679
+ 680my_data=None 681
- 682defload_iptv(self)->bool:
- 683"""Load XTream IPTV
- 684
- 685 - Add all Live TV to XTream.channels
- 686 - Add all VOD to XTream.movies
- 687 - Add all Series to XTream.series
- 688 Series contains Seasons and Episodes. Those are not automatically
- 689 retrieved from the server to reduce the loading time.
- 690 - Add all groups to XTream.groups
- 691 Groups are for all three channel types, Live TV, VOD, and Series
- 692
- 693 Returns:
- 694 bool: True if successfull, False if error
- 695 """
- 696# If pyxtream has not authenticated the connection, return empty
- 697ifself.state["authenticated"]isFalse:
- 698print("Warning, cannot load steams since authorization failed")
- 699returnFalse
- 700
- 701# If pyxtream has already loaded the data, skip and return success
- 702ifself.state["loaded"]isTrue:
- 703print("Warning, data has already been loaded.")
- 704returnTrue
- 705
- 706forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
- 707## Get GROUPS
+ 682# Get the enlapsed seconds since last file update
+ 683file_age_sec=time.time()-osp.getmtime(full_filename)
+ 684# If the file was updated less than the threshold time,
+ 685# it means that the file is still fresh, we can load it.
+ 686# Otherwise skip and return None to force a re-download
+ 687ifself.threshold_time_sec>file_age_sec:
+ 688# Load the JSON data
+ 689try:
+ 690withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
+ 691my_data=json.load(myfile)
+ 692iflen(my_data)==0:
+ 693my_data=None
+ 694exceptExceptionase:
+ 695print(f" - Could not load from file `{full_filename}`: e=`{e}`")
+ 696returnmy_data
+ 697
+ 698returnNone
+ 699
+ 700def_save_to_file(self,data_list:dict,filename:str)->bool:
+ 701"""Save a dictionary to file
+ 702
+ 703 This function will overwrite the file if already exists
+ 704
+ 705 Args:
+ 706 data_list (dict): Dictionary to save
+ 707 filename (str): Name of the file 708
- 709# Try loading local file
- 710dt=0
- 711start=timer()
- 712all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
- 713# If file empty or does not exists, download it from remote
- 714ifall_catisNone:
- 715# Load all Groups and save file locally
- 716all_cat=self._load_categories_from_provider(loading_stream_type)
- 717ifall_catisnotNone:
- 718self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
- 719dt=timer()-start
- 720
- 721# If we got the GROUPS data, show the statistics and load GROUPS
- 722ifall_catisnotNone:
- 723print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
- 724## Add GROUPS to dictionaries
- 725
- 726# Add the catch-all-errors group
- 727ifloading_stream_type==self.live_type:
- 728self.groups.append(self.live_catch_all_group)
- 729elifloading_stream_type==self.vod_type:
- 730self.groups.append(self.vod_catch_all_group)
- 731elifloading_stream_type==self.series_type:
- 732self.groups.append(self.series_catch_all_group)
- 733
- 734forcat_objinall_cat:
- 735ifschemaValidator(cat_obj,SchemaType.GROUP):
- 736# Create Group (Category)
- 737new_group=Group(cat_obj,loading_stream_type)
- 738# Add to xtream class
- 739self.groups.append(new_group)
- 740else:
- 741# Save what did not pass schema validation
- 742print(cat_obj)
- 743
- 744# Sort Categories
- 745self.groups.sort(key=lambdax:x.name)
- 746else:
- 747print(f" - Could not load {loading_stream_type} Groups")
- 748break
- 749
- 750## Get Streams
- 751
- 752# Try loading local file
- 753dt=0
- 754start=timer()
- 755all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
- 756# If file empty or does not exists, download it from remote
- 757ifall_streamsisNone:
- 758# Load all Streams and save file locally
- 759all_streams=self._load_streams_from_provider(loading_stream_type)
- 760self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
- 761dt=timer()-start
- 762
- 763# If we got the STREAMS data, show the statistics and load Streams
- 764ifall_streamsisnotNone:
- 765print(
- 766f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams " \
- 767f"in {dt:.3f} seconds"
- 768)
- 769## Add Streams to dictionaries
- 770
- 771skipped_adult_content=0
- 772skipped_no_name_content=0
- 773
- 774number_of_streams=len(all_streams)
- 775current_stream_number=0
- 776# Calculate 1% of total number of streams
- 777# This is used to slow down the progress bar
- 778one_percent_number_of_streams=number_of_streams/100
- 779start=timer()
- 780forstream_channelinall_streams:
- 781skip_stream=False
- 782current_stream_number+=1
+ 709 Returns:
+ 710 bool: True if successfull, False if error
+ 711 """
+ 712ifdata_listisNone:
+ 713returnFalse
+ 714
+ 715full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 716try:
+ 717withopen(full_filename,mode="wt",encoding="utf-8")asfile:
+ 718json.dump(data_list,file,ensure_ascii=False)
+ 719returnTrue
+ 720exceptExceptionase:
+ 721print(f" - Could not save to file `{full_filename}`: e=`{e}`")
+ 722returnFalse
+ 723# if data_list is not None:
+ 724
+ 725# #Build the full path
+ 726# full_filename = osp.join(self.cache_path, f"{self._slugify(self.name)}-{filename}")
+ 727# # If the path makes sense, save the file
+ 728# json_data = json.dumps(data_list, ensure_ascii=False)
+ 729# try:
+ 730# with open(full_filename, mode="wt", encoding="utf-8") as myfile:
+ 731# myfile.write(json_data)
+ 732# except Exception as e:
+ 733# print(f" - Could not save to file `{full_filename}`: e=`{e}`")
+ 734# return False
+ 735
+ 736# return True
+ 737# else:
+ 738# return False
+ 739
+ 740defload_iptv(self)->bool:
+ 741"""Load XTream IPTV
+ 742
+ 743 - Add all Live TV to XTream.channels
+ 744 - Add all VOD to XTream.movies
+ 745 - Add all Series to XTream.series
+ 746 Series contains Seasons and Episodes. Those are not automatically
+ 747 retrieved from the server to reduce the loading time.
+ 748 - Add all groups to XTream.groups
+ 749 Groups are for all three channel types, Live TV, VOD, and Series
+ 750
+ 751 Returns:
+ 752 bool: True if successfull, False if error
+ 753 """
+ 754# If pyxtream has not authenticated the connection, return empty
+ 755ifself.state["authenticated"]isFalse:
+ 756print("Warning, cannot load steams since authorization failed")
+ 757returnFalse
+ 758
+ 759# If pyxtream has already loaded the data, skip and return success
+ 760ifself.state["loaded"]isTrue:
+ 761print("Warning, data has already been loaded.")
+ 762returnTrue
+ 763
+ 764forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
+ 765## Get GROUPS
+ 766
+ 767# Try loading local file
+ 768dt=0
+ 769start=timer()
+ 770all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
+ 771# If file empty or does not exists, download it from remote
+ 772ifall_catisNone:
+ 773# Load all Groups and save file locally
+ 774all_cat=self._load_categories_from_provider(loading_stream_type)
+ 775ifall_catisnotNone:
+ 776self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
+ 777dt=timer()-start
+ 778
+ 779# If we got the GROUPS data, show the statistics and load GROUPS
+ 780ifall_catisnotNone:
+ 781print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
+ 782## Add GROUPS to dictionaries 783
- 784# Show download progress every 1% of total number of streams
- 785ifcurrent_stream_number<one_percent_number_of_streams:
- 786progress(
- 787current_stream_number,
- 788number_of_streams,
- 789f"Processing {loading_stream_type} Streams"
- 790)
- 791one_percent_number_of_streams*=2
- 792
- 793# Validate JSON scheme
- 794ifself.validate_json:
- 795ifloading_stream_type==self.series_type:
- 796ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
- 797print(stream_channel)
- 798elifloading_stream_type==self.live_type:
- 799ifnotschemaValidator(stream_channel,SchemaType.LIVE):
- 800print(stream_channel)
- 801else:
- 802# vod_type
- 803ifnotschemaValidator(stream_channel,SchemaType.VOD):
- 804print(stream_channel)
- 805
- 806# Skip if the name of the stream is empty
- 807ifstream_channel["name"]=="":
- 808skip_stream=True
- 809skipped_no_name_content=skipped_no_name_content+1
- 810self._save_to_file_skipped_streams(stream_channel)
- 811
- 812# Skip if the user chose to hide adult streams
- 813ifself.hide_adult_contentandloading_stream_type==self.live_type:
- 814if"is_adult"instream_channel:
- 815ifstream_channel["is_adult"]=="1":
- 816skip_stream=True
- 817skipped_adult_content=skipped_adult_content+1
- 818self._save_to_file_skipped_streams(stream_channel)
- 819
- 820ifnotskip_stream:
- 821# Some channels have no group,
- 822# so let's add them to the catch all group
- 823ifstream_channel["category_id"]isNone:
- 824stream_channel["category_id"]="9999"
- 825elifstream_channel["category_id"]!="1":
- 826pass
- 827
- 828# Find the first occurence of the group that the
- 829# Channel or Stream is pointing to
- 830the_group=next(
- 831(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
- 832None
- 833)
- 834
- 835# Set group title
- 836ifthe_groupisnotNone:
- 837group_title=the_group.name
- 838else:
- 839ifloading_stream_type==self.live_type:
- 840group_title=self.live_catch_all_group.name
- 841the_group=self.live_catch_all_group
- 842elifloading_stream_type==self.vod_type:
- 843group_title=self.vod_catch_all_group.name
- 844the_group=self.vod_catch_all_group
- 845elifloading_stream_type==self.series_type:
- 846group_title=self.series_catch_all_group.name
- 847the_group=self.series_catch_all_group
- 848
- 849
+ 784# Add the catch-all-errors group
+ 785ifloading_stream_type==self.live_type:
+ 786self.groups.append(self.live_catch_all_group)
+ 787elifloading_stream_type==self.vod_type:
+ 788self.groups.append(self.vod_catch_all_group)
+ 789elifloading_stream_type==self.series_type:
+ 790self.groups.append(self.series_catch_all_group)
+ 791
+ 792forcat_objinall_cat:
+ 793ifschemaValidator(cat_obj,SchemaType.GROUP):
+ 794# Create Group (Category)
+ 795new_group=Group(cat_obj,loading_stream_type)
+ 796# Add to xtream class
+ 797self.groups.append(new_group)
+ 798else:
+ 799# Save what did not pass schema validation
+ 800print(cat_obj)
+ 801
+ 802# Sort Categories
+ 803self.groups.sort(key=lambdax:x.name)
+ 804else:
+ 805print(f" - Could not load {loading_stream_type} Groups")
+ 806break
+ 807
+ 808## Get Streams
+ 809
+ 810# Try loading local file
+ 811dt=0
+ 812start=timer()
+ 813all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
+ 814# If file empty or does not exists, download it from remote
+ 815ifall_streamsisNone:
+ 816# Load all Streams and save file locally
+ 817all_streams=self._load_streams_from_provider(loading_stream_type)
+ 818self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
+ 819dt=timer()-start
+ 820
+ 821# If we got the STREAMS data, show the statistics and load Streams
+ 822ifall_streamsisnotNone:
+ 823print(f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams in {dt:.3f} seconds")
+ 824## Add Streams to dictionaries
+ 825
+ 826skipped_adult_content=0
+ 827skipped_no_name_content=0
+ 828
+ 829number_of_streams=len(all_streams)
+ 830current_stream_number=0
+ 831# Calculate 1% of total number of streams
+ 832# This is used to slow down the progress bar
+ 833one_percent_number_of_streams=number_of_streams/100
+ 834start=timer()
+ 835forstream_channelinall_streams:
+ 836skip_stream=False
+ 837current_stream_number+=1
+ 838
+ 839# Show download progress every 1% of total number of streams
+ 840ifcurrent_stream_number<one_percent_number_of_streams:
+ 841progress(
+ 842current_stream_number,
+ 843number_of_streams,
+ 844f"Processing {loading_stream_type} Streams"
+ 845)
+ 846one_percent_number_of_streams*=2
+ 847
+ 848# Validate JSON scheme
+ 849ifself.validate_json: 850ifloading_stream_type==self.series_type:
- 851# Load all Series
- 852new_series=Serie(self,stream_channel)
- 853# To get all the Episodes for every Season of each
- 854# Series is very time consuming, we will only
- 855# populate the Series once the user click on the
- 856# Series, the Seasons and Episodes will be loaded
- 857# using x.getSeriesInfoByID() function
- 858
- 859else:
- 860new_channel=Channel(
- 861self,
- 862group_title,
- 863stream_channel
- 864)
- 865
- 866ifnew_channel.group_id=="9999":
- 867print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
- 868
- 869# Save the new channel to the local list of channels
- 870ifloading_stream_type==self.live_type:
- 871self.channels.append(new_channel)
- 872elifloading_stream_type==self.vod_type:
- 873self.movies.append(new_channel)
- 874ifnew_channel.age_days_from_added<31:
- 875self.movies_30days.append(new_channel)
- 876ifnew_channel.age_days_from_added<7:
- 877self.movies_7days.append(new_channel)
- 878else:
- 879self.series.append(new_series)
- 880
- 881# Add stream to the specific Group
- 882ifthe_groupisnotNone:
- 883ifloading_stream_type!=self.series_type:
- 884the_group.channels.append(new_channel)
- 885else:
- 886the_group.series.append(new_series)
- 887else:
- 888print(f" - Group not found `{stream_channel['name']}`")
- 889print("\n")
- 890# Print information of which streams have been skipped
- 891ifself.hide_adult_content:
- 892print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
- 893ifskipped_no_name_content>0:
- 894print(f" - Skipped {skipped_no_name_content} unprintable {loading_stream_type} streams")
- 895else:
- 896print(f" - Could not load {loading_stream_type} Streams")
- 897
- 898self.state["loaded"]=True
- 899
- 900def_save_to_file_skipped_streams(self,stream_channel:Channel):
- 901
- 902# Build the full path
- 903full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 851ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
+ 852print(stream_channel)
+ 853elifloading_stream_type==self.live_type:
+ 854ifnotschemaValidator(stream_channel,SchemaType.LIVE):
+ 855print(stream_channel)
+ 856else:
+ 857# vod_type
+ 858ifnotschemaValidator(stream_channel,SchemaType.VOD):
+ 859print(stream_channel)
+ 860
+ 861# Skip if the name of the stream is empty
+ 862ifstream_channel["name"]=="":
+ 863skip_stream=True
+ 864skipped_no_name_content=skipped_no_name_content+1
+ 865self._save_to_file_skipped_streams(stream_channel)
+ 866
+ 867# Skip if the user chose to hide adult streams
+ 868ifself.hide_adult_contentandloading_stream_type==self.live_type:
+ 869if"is_adult"instream_channel:
+ 870ifstream_channel["is_adult"]=="1":
+ 871skip_stream=True
+ 872skipped_adult_content=skipped_adult_content+1
+ 873self._save_to_file_skipped_streams(stream_channel)
+ 874
+ 875ifnotskip_stream:
+ 876# Some channels have no group,
+ 877# so let's add them to the catch all group
+ 878ifstream_channel["category_id"]=="":
+ 879stream_channel["category_id"]="9999"
+ 880elifstream_channel["category_id"]!="1":
+ 881pass
+ 882
+ 883# Find the first occurence of the group that the
+ 884# Channel or Stream is pointing to
+ 885the_group=next(
+ 886(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
+ 887None
+ 888)
+ 889
+ 890# Set group title
+ 891ifthe_groupisnotNone:
+ 892group_title=the_group.name
+ 893else:
+ 894ifloading_stream_type==self.live_type:
+ 895group_title=self.live_catch_all_group.name
+ 896the_group=self.live_catch_all_group
+ 897elifloading_stream_type==self.vod_type:
+ 898group_title=self.vod_catch_all_group.name
+ 899the_group=self.vod_catch_all_group
+ 900elifloading_stream_type==self.series_type:
+ 901group_title=self.series_catch_all_group.name
+ 902the_group=self.series_catch_all_group
+ 903 904
- 905# If the path makes sense, save the file
- 906json_data=json.dumps(stream_channel,ensure_ascii=False)
- 907try:
- 908withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
- 909myfile.writelines(json_data)
- 910returnTrue
- 911exceptExceptionase:
- 912print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
- 913returnFalse
- 914
- 915defget_series_info_by_id(self,get_series:dict):
- 916"""Get Seasons and Episodes for a Series
- 917
- 918 Args:
- 919 get_series (dict): Series dictionary
- 920 """
- 921
- 922series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
+ 905ifloading_stream_type==self.series_type:
+ 906# Load all Series
+ 907new_series=Serie(self,stream_channel)
+ 908# To get all the Episodes for every Season of each
+ 909# Series is very time consuming, we will only
+ 910# populate the Series once the user click on the
+ 911# Series, the Seasons and Episodes will be loaded
+ 912# using x.getSeriesInfoByID() function
+ 913
+ 914else:
+ 915new_channel=Channel(
+ 916self,
+ 917group_title,
+ 918stream_channel
+ 919)
+ 920
+ 921ifnew_channel.group_id=="9999":
+ 922print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}") 923
- 924ifseries_seasons["seasons"]isNone:
- 925series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
- 926
- 927forseries_infoinseries_seasons["seasons"]:
- 928season_name=series_info["name"]
- 929season_key=series_info['season_number']
- 930season=Season(season_name)
- 931get_series.seasons[season_name]=season
- 932if"episodes"inseries_seasons.keys():
- 933forseries_seasoninseries_seasons["episodes"].keys():
- 934forepisode_infoinseries_seasons["episodes"][str(series_season)]:
- 935new_episode_channel=Episode(
- 936self,series_info,"Testing",episode_info
- 937)
- 938season.episodes[episode_info["title"]]=new_episode_channel
- 939
- 940def_get_request(self,url:str,timeout:Tuple=(2,15)):
- 941"""Generic GET Request with Error handling
- 942
- 943 Args:
- 944 URL (str): The URL where to GET content
- 945 timeout (Tuple, optional): Connection and Downloading Timeout. Defaults to (2,15).
- 946
- 947 Returns:
- 948 [type]: JSON dictionary of the loaded data, or None
- 949 """
- 950i=0
- 951whilei<10:
- 952time.sleep(1)
- 953try:
- 954r=requests.get(url,timeout=timeout,headers=self.connection_headers)
- 955i=20
- 956ifr.status_code==200:
- 957returnr.json()
- 958exceptrequests.exceptions.ConnectionError:
- 959print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
- 960i+=1
- 961
- 962exceptrequests.exceptions.HTTPError:
- 963print(" - HTTP Error")
- 964i+=1
- 965
- 966exceptrequests.exceptions.TooManyRedirects:
- 967print(" - TooManyRedirects")
- 968i+=1
- 969
- 970exceptrequests.exceptions.ReadTimeout:
- 971print(" - Timeout while loading data")
- 972i+=1
+ 924# Save the new channel to the local list of channels
+ 925ifloading_stream_type==self.live_type:
+ 926self.channels.append(new_channel)
+ 927elifloading_stream_type==self.vod_type:
+ 928self.movies.append(new_channel)
+ 929ifnew_channel.age_days_from_added<31:
+ 930self.movies_30days.append(new_channel)
+ 931ifnew_channel.age_days_from_added<7:
+ 932self.movies_7days.append(new_channel)
+ 933else:
+ 934self.series.append(new_series)
+ 935
+ 936# Add stream to the specific Group
+ 937ifthe_groupisnotNone:
+ 938ifloading_stream_type!=self.series_type:
+ 939the_group.channels.append(new_channel)
+ 940else:
+ 941the_group.series.append(new_series)
+ 942else:
+ 943print(f" - Group not found `{stream_channel['name']}`")
+ 944print("\n")
+ 945# Print information of which streams have been skipped
+ 946ifself.hide_adult_content:
+ 947print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
+ 948ifskipped_no_name_content>0:
+ 949print(f" - Skipped {skipped_no_name_content} "
+ 950"unprintable {loading_stream_type} streams")
+ 951else:
+ 952print(f" - Could not load {loading_stream_type} Streams")
+ 953
+ 954self.state["loaded"]=True
+ 955
+ 956def_save_to_file_skipped_streams(self,stream_channel:Channel):
+ 957
+ 958# Build the full path
+ 959full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 960
+ 961# If the path makes sense, save the file
+ 962json_data=json.dumps(stream_channel,ensure_ascii=False)
+ 963try:
+ 964withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
+ 965myfile.writelines(json_data)
+ 966returnTrue
+ 967exceptExceptionase:
+ 968print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
+ 969returnFalse
+ 970
+ 971defget_series_info_by_id(self,get_series:dict):
+ 972"""Get Seasons and Episodes for a Series 973
- 974returnNone
- 975
- 976# GET Stream Categories
- 977def_load_categories_from_provider(self,stream_type:str):
- 978"""Get from provider all category for specific stream type from provider
+ 974 Args:
+ 975 get_series (dict): Series dictionary
+ 976 """
+ 977
+ 978series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id) 979
- 980 Args:
- 981 stream_type (str): Stream type can be Live, VOD, Series
+ 980ifseries_seasons["seasons"]isNone:
+ 981series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}] 982
- 983 Returns:
- 984 [type]: JSON if successfull, otherwise None
- 985 """
- 986url=""
- 987ifstream_type==self.live_type:
- 988url=self.get_live_categories_URL()
- 989elifstream_type==self.vod_type:
- 990url=self.get_vod_cat_URL()
- 991elifstream_type==self.series_type:
- 992url=self.get_series_cat_URL()
- 993else:
- 994url=""
+ 983forseries_infoinseries_seasons["seasons"]:
+ 984season_name=series_info["name"]
+ 985season_key=series_info['season_number']
+ 986season=Season(season_name)
+ 987get_series.seasons[season_name]=season
+ 988if"episodes"inseries_seasons.keys():
+ 989forseries_seasoninseries_seasons["episodes"].keys():
+ 990forepisode_infoinseries_seasons["episodes"][str(series_season)]:
+ 991new_episode_channel=Episode(
+ 992self,series_info,"Testing",episode_info
+ 993)
+ 994season.episodes[episode_info["title"]]=new_episode_channel 995
- 996returnself._get_request(url)
- 997
- 998# GET Streams
- 999def_load_streams_from_provider(self,stream_type:str):
-1000"""Get from provider all streams for specific stream type
-1001
-1002 Args:
-1003 stream_type (str): Stream type can be Live, VOD, Series
-1004
-1005 Returns:
-1006 [type]: JSON if successfull, otherwise None
-1007 """
-1008url=""
-1009ifstream_type==self.live_type:
-1010url=self.get_live_streams_URL()
-1011elifstream_type==self.vod_type:
-1012url=self.get_vod_streams_URL()
-1013elifstream_type==self.series_type:
-1014url=self.get_series_URL()
-1015else:
-1016url=""
+ 996def_handle_request_exception(self,exception:requests.exceptions.RequestException):
+ 997"""Handle different types of request exceptions."""
+ 998ifisinstance(exception,requests.exceptions.ConnectionError):
+ 999print(" - Connection Error: Possible network problem \
+1000 (e.g. DNS failure, refused connection, etc)")
+1001elifisinstance(exception,requests.exceptions.HTTPError):
+1002print(" - HTTP Error")
+1003elifisinstance(exception,requests.exceptions.TooManyRedirects):
+1004print(" - TooManyRedirects")
+1005elifisinstance(exception,requests.exceptions.ReadTimeout):
+1006print(" - Timeout while loading data")
+1007else:
+1008print(f" - An unexpected error occurred: {exception}")
+1009
+1010def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:
+1011"""Generic GET Request with Error handling
+1012
+1013 Args:
+1014 URL (str): The URL where to GET content
+1015 timeout (Tuple[int, int], optional): Connection and Downloading Timeout.
+1016 Defaults to (2,15).1017
-1018returnself._get_request(url)
-1019
-1020# GET Streams by Category
-1021def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
-1022"""Get from provider all streams for specific stream type with category/group ID
-1023
-1024 Args:
-1025 stream_type (str): Stream type can be Live, VOD, Series
-1026 category_id ([type]): Category/Group ID.
-1027
-1028 Returns:
-1029 [type]: JSON if successfull, otherwise None
-1030 """
-1031url=""
-1032
-1033ifstream_type==self.live_type:
-1034url=self.get_live_streams_URL_by_category(category_id)
-1035elifstream_type==self.vod_type:
-1036url=self.get_vod_streams_URL_by_category(category_id)
-1037elifstream_type==self.series_type:
-1038url=self.get_series_URL_by_category(category_id)
-1039else:
-1040url=""
-1041
-1042returnself._get_request(url)
-1043
-1044# GET SERIES Info
-1045def_load_series_info_by_id_from_provider(self,series_id:str):
-1046"""Gets informations about a Serie
-1047
-1048 Args:
-1049 series_id (str): Serie ID as described in Group
+1018 Returns:
+1019 Optional[dict]: JSON dictionary of the loaded data, or None
+1020 """
+1021forattemptinrange(10):
+1022time.sleep(1)
+1023try:
+1024response=requests.get(url,timeout=timeout,headers=self.connection_headers)
+1025response.raise_for_status()# Raise an HTTPError for bad responses (4xx and 5xx)
+1026returnresponse.json()
+1027exceptrequests.exceptions.RequestExceptionase:
+1028self._handle_request_exception(e)
+1029
+1030returnNone
+1031# i = 0
+1032# while i < 10:
+1033# time.sleep(1)
+1034# try:
+1035# r = requests.get(url, timeout=timeout, headers=self.connection_headers)
+1036# i = 20
+1037# if r.status_code == 200:
+1038# return r.json()
+1039# except requests.exceptions.ConnectionError:
+1040# print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
+1041# i += 1
+1042
+1043# except requests.exceptions.HTTPError:
+1044# print(" - HTTP Error")
+1045# i += 1
+1046
+1047# except requests.exceptions.TooManyRedirects:
+1048# print(" - TooManyRedirects")
+1049# i += 11050
-1051 Returns:
-1052 [type]: JSON if successfull, otherwise None
-1053 """
-1054returnself._get_request(self.get_series_info_URL_by_ID(series_id))
-1055
-1056# The seasons array, might be filled or might be completely empty.
-1057# If it is not empty, it will contain the cover, overview and the air date
-1058# of the selected season.
-1059# In your APP if you want to display the series, you have to take that
-1060# from the episodes array.
-1061
-1062# GET VOD Info
-1063defvodInfoByID(self,vod_id):
-1064returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
-1065
-1066# GET short_epg for LIVE Streams (same as stalker portal,
-1067# prints the next X EPG that will play soon)
-1068defliveEpgByStream(self,stream_id):
-1069returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
-1070
-1071defliveEpgByStreamAndLimit(self,stream_id,limit):
-1072returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
-1073
-1074# GET ALL EPG for LIVE Streams (same as stalker portal,
-1075# but it will print all epg listings regardless of the day)
-1076defallLiveEpgByStream(self,stream_id):
-1077returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
+1051# except requests.exceptions.ReadTimeout:
+1052# print(" - Timeout while loading data")
+1053# i += 1
+1054
+1055# return None
+1056
+1057# GET Stream Categories
+1058def_load_categories_from_provider(self,stream_type:str):
+1059"""Get from provider all category for specific stream type from provider
+1060
+1061 Args:
+1062 stream_type (str): Stream type can be Live, VOD, Series
+1063
+1064 Returns:
+1065 [type]: JSON if successfull, otherwise None
+1066 """
+1067url=""
+1068ifstream_type==self.live_type:
+1069url=self.get_live_categories_URL()
+1070elifstream_type==self.vod_type:
+1071url=self.get_vod_cat_URL()
+1072elifstream_type==self.series_type:
+1073url=self.get_series_cat_URL()
+1074else:
+1075url=""
+1076
+1077returnself._get_request(url)1078
-1079# Full EPG List for all Streams
-1080defallEpg(self):
-1081returnself._get_request(self.get_all_epg_URL())
+1079# GET Streams
+1080def_load_streams_from_provider(self,stream_type:str):
+1081"""Get from provider all streams for specific stream type1082
-1083## URL-builder methods
-1084defget_live_categories_URL(self)->str:
-1085returnf"{self.base_url}&action=get_live_categories"
-1086
-1087defget_live_streams_URL(self)->str:
-1088returnf"{self.base_url}&action=get_live_streams"
-1089
-1090defget_live_streams_URL_by_category(self,category_id)->str:
-1091returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
-1092
-1093defget_vod_cat_URL(self)->str:
-1094returnf"{self.base_url}&action=get_vod_categories"
-1095
-1096defget_vod_streams_URL(self)->str:
-1097returnf"{self.base_url}&action=get_vod_streams"
+1083 Args:
+1084 stream_type (str): Stream type can be Live, VOD, Series
+1085
+1086 Returns:
+1087 [type]: JSON if successfull, otherwise None
+1088 """
+1089url=""
+1090ifstream_type==self.live_type:
+1091url=self.get_live_streams_URL()
+1092elifstream_type==self.vod_type:
+1093url=self.get_vod_streams_URL()
+1094elifstream_type==self.series_type:
+1095url=self.get_series_URL()
+1096else:
+1097url=""1098
-1099defget_vod_streams_URL_by_category(self,category_id)->str:
-1100returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
-1101
-1102defget_series_cat_URL(self)->str:
-1103returnf"{self.base_url}&action=get_series_categories"
+1099returnself._get_request(url)
+1100
+1101# GET Streams by Category
+1102def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
+1103"""Get from provider all streams for specific stream type with category/group ID1104
-1105defget_series_URL(self)->str:
-1106returnf"{self.base_url}&action=get_series"
-1107
-1108defget_series_URL_by_category(self,category_id)->str:
-1109returnf"{self.base_url}&action=get_series&category_id={category_id}"
-1110
-1111defget_series_info_URL_by_ID(self,series_id)->str:
-1112returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
+1105 Args:
+1106 stream_type (str): Stream type can be Live, VOD, Series
+1107 category_id ([type]): Category/Group ID.
+1108
+1109 Returns:
+1110 [type]: JSON if successfull, otherwise None
+1111 """
+1112url=""1113
-1114defget_VOD_info_URL_by_ID(self,vod_id)->str:
-1115returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
-1116
-1117defget_live_epg_URL_by_stream(self,stream_id)->str:
-1118returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
-1119
-1120defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
-1121returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
+1114ifstream_type==self.live_type:
+1115url=self.get_live_streams_URL_by_category(category_id)
+1116elifstream_type==self.vod_type:
+1117url=self.get_vod_streams_URL_by_category(category_id)
+1118elifstream_type==self.series_type:
+1119url=self.get_series_URL_by_category(category_id)
+1120else:
+1121url=""1122
-1123defget_all_live_epg_URL_by_stream(self,stream_id)->str:
-1124returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
-1125
-1126defget_all_epg_URL(self)->str:
-1127returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
+1123returnself._get_request(url)
+1124
+1125# GET SERIES Info
+1126def_load_series_info_by_id_from_provider(self,series_id:str):
+1127"""Gets informations about a Serie
+1128
+1129 Args:
+1130 series_id (str): Serie ID as described in Group
+1131
+1132 Returns:
+1133 [type]: JSON if successfull, otherwise None
+1134 """
+1135returnself._get_request(self.get_series_info_URL_by_ID(series_id))
+1136
+1137# The seasons array, might be filled or might be completely empty.
+1138# If it is not empty, it will contain the cover, overview and the air date
+1139# of the selected season.
+1140# In your APP if you want to display the series, you have to take that
+1141# from the episodes array.
+1142
+1143# GET VOD Info
+1144defvodInfoByID(self,vod_id):
+1145returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
+1146
+1147# GET short_epg for LIVE Streams (same as stalker portal,
+1148# prints the next X EPG that will play soon)
+1149defliveEpgByStream(self,stream_id):
+1150returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
+1151
+1152defliveEpgByStreamAndLimit(self,stream_id,limit):
+1153returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
+1154
+1155# GET ALL EPG for LIVE Streams (same as stalker portal,
+1156# but it will print all epg listings regardless of the day)
+1157defallLiveEpgByStream(self,stream_id):
+1158returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
+1159
+1160# Full EPG List for all Streams
+1161defallEpg(self):
+1162returnself._get_request(self.get_all_epg_URL())
+1163
+1164## URL-builder methods
+1165defget_live_categories_URL(self)->str:
+1166returnf"{self.base_url}&action=get_live_categories"
+1167
+1168defget_live_streams_URL(self)->str:
+1169returnf"{self.base_url}&action=get_live_streams"
+1170
+1171defget_live_streams_URL_by_category(self,category_id)->str:
+1172returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
+1173
+1174defget_vod_cat_URL(self)->str:
+1175returnf"{self.base_url}&action=get_vod_categories"
+1176
+1177defget_vod_streams_URL(self)->str:
+1178returnf"{self.base_url}&action=get_vod_streams"
+1179
+1180defget_vod_streams_URL_by_category(self,category_id)->str:
+1181returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
+1182
+1183defget_series_cat_URL(self)->str:
+1184returnf"{self.base_url}&action=get_series_categories"
+1185
+1186defget_series_URL(self)->str:
+1187returnf"{self.base_url}&action=get_series"
+1188
+1189defget_series_URL_by_category(self,category_id)->str:
+1190returnf"{self.base_url}&action=get_series&category_id={category_id}"
+1191
+1192defget_series_info_URL_by_ID(self,series_id)->str:
+1193returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
+1194
+1195defget_VOD_info_URL_by_ID(self,vod_id)->str:
+1196returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
+1197
+1198defget_live_epg_URL_by_stream(self,stream_id)->str:
+1199returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
+1200
+1201defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
+1202returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
+1203
+1204defget_all_live_epg_URL_by_stream(self,stream_id)->str:
+1205returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
+1206
+1207defget_all_epg_URL(self)->str:
+1208returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
@@ -2826,789 +2907,870 @@
343 provider_url (str): URL of the IPTV provider 344 headers (dict): Requests Headers 345 hide_adult_content(bool, optional): When `True` hide stream that are marked for adult
- 346 cache_path (str, optional): Location where to save loaded files. Defaults to empty string.
- 347 reload_time_sec (int, optional): Number of seconds before automatic reloading (-1 to turn it OFF)
- 348 debug_flask (bool, optional): Enable the debug mode in Flask
- 349 validate_json (bool, optional): Check Xtream API provided JSON for validity
- 350
- 351 Returns: XTream Class Instance
+ 346 cache_path (str, optional): Location where to save loaded files.
+ 347 Defaults to empty string.
+ 348 reload_time_sec (int, optional): Number of seconds before automatic reloading
+ 349 (-1 to turn it OFF)
+ 350 debug_flask (bool, optional): Enable the debug mode in Flask
+ 351 validate_json (bool, optional): Check Xtream API provided JSON for validity 352
- 353 - Note 1: If it fails to authorize with provided username and password,
- 354 auth_data will be an empty dictionary.
- 355 - Note 2: The JSON validation option will take considerable amount of time and it should be
- 356 used only as a debug tool. The Xtream API JSON from the provider passes through a schema
- 357 that represent the best available understanding of how the Xtream API works.
- 358 """
- 359self.server=provider_url
- 360self.username=provider_username
- 361self.password=provider_password
- 362self.name=provider_name
- 363self.cache_path=cache_path
- 364self.hide_adult_content=hide_adult_content
- 365self.threshold_time_sec=reload_time_sec
- 366self.validate_json=validate_json
- 367
- 368# get the pyxtream local path
- 369self.app_fullpath=osp.dirname(osp.realpath(__file__))
+ 353 Returns: XTream Class Instance
+ 354
+ 355 - Note 1: If it fails to authorize with provided username and password,
+ 356 auth_data will be an empty dictionary.
+ 357 - Note 2: The JSON validation option will take considerable amount of time and it should be
+ 358 used only as a debug tool. The Xtream API JSON from the provider passes through a
+ 359 schema that represent the best available understanding of how the Xtream API
+ 360 works.
+ 361 """
+ 362self.server=provider_url
+ 363self.username=provider_username
+ 364self.password=provider_password
+ 365self.name=provider_name
+ 366self.cache_path=cache_path
+ 367self.hide_adult_content=hide_adult_content
+ 368self.threshold_time_sec=reload_time_sec
+ 369self.validate_json=validate_json 370
- 371# prepare location of local html template
- 372self.html_template_folder=osp.join(self.app_fullpath,"html")
+ 371# get the pyxtream local path
+ 372self.app_fullpath=osp.dirname(osp.realpath(__file__)) 373
- 374# if the cache_path is specified, test that it is a directory
- 375ifself.cache_path!="":
- 376# If the cache_path is not a directory, clear it
- 377ifnotosp.isdir(self.cache_path):
- 378print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
- 379self.cache_path==""
- 380
- 381# If the cache_path is still empty, use default
- 382ifself.cache_path=="":
- 383self.cache_path=osp.expanduser("~/.xtream-cache/")
- 384ifnotosp.isdir(self.cache_path):
- 385makedirs(self.cache_path,exist_ok=True)
- 386print(f"pyxtream cache path located at {self.cache_path}")
- 387
- 388ifheadersisnotNone:
- 389self.connection_headers=headers
- 390else:
- 391self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
- 392
- 393self.authenticate()
- 394
- 395ifself.threshold_time_sec>0:
- 396print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
- 397else:
- 398print("Reload timer is OFF")
- 399
- 400ifself.state['authenticated']:
- 401ifUSE_FLASK:
- 402self.flaskapp=FlaskWrap('pyxtream',self,self.html_template_folder,debug=debug_flask)
- 403self.flaskapp.start()
- 404
- 405defsearch_stream(self,keyword:str,ignore_case:bool=True,return_type:str="LIST")->List:
- 406"""Search for streams
+ 374# prepare location of local html template
+ 375self.html_template_folder=osp.join(self.app_fullpath,"html")
+ 376
+ 377# if the cache_path is specified, test that it is a directory
+ 378ifself.cache_path!="":
+ 379# If the cache_path is not a directory, clear it
+ 380ifnotosp.isdir(self.cache_path):
+ 381print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
+ 382self.cache_path==""
+ 383
+ 384# If the cache_path is still empty, use default
+ 385ifself.cache_path=="":
+ 386self.cache_path=osp.expanduser("~/.xtream-cache/")
+ 387ifnotosp.isdir(self.cache_path):
+ 388makedirs(self.cache_path,exist_ok=True)
+ 389print(f"pyxtream cache path located at {self.cache_path}")
+ 390
+ 391ifheadersisnotNone:
+ 392self.connection_headers=headers
+ 393else:
+ 394self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
+ 395
+ 396self.authenticate()
+ 397
+ 398ifself.threshold_time_sec>0:
+ 399print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
+ 400else:
+ 401print("Reload timer is OFF")
+ 402
+ 403ifself.state['authenticated']:
+ 404ifUSE_FLASK:
+ 405self.flaskapp=FlaskWrap('pyxtream',self,self.html_template_folder,debug=debug_flask)
+ 406self.flaskapp.start() 407
- 408 Args:
- 409 keyword (str): Keyword to search for. Supports REGEX
- 410 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
- 411 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
- 412
- 413 Returns:
- 414 List: List with all the results, it could be empty. Each result
- 415 """
- 416
- 417search_result=[]
- 418
- 419ifignore_case:
- 420regex=re.compile(keyword,re.IGNORECASE)
- 421else:
- 422regex=re.compile(keyword)
+ 408defsearch_stream(self,keyword:str,
+ 409ignore_case:bool=True,
+ 410return_type:str="LIST",
+ 411stream_type:list=("series","movies","channels"))->list:
+ 412"""Search for streams
+ 413
+ 414 Args:
+ 415 keyword (str): Keyword to search for. Supports REGEX
+ 416 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
+ 417 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
+ 418 stream_type (list, optional): Search within specific stream type.
+ 419
+ 420 Returns:
+ 421 list: List with all the results, it could be empty.
+ 422 """ 423
- 424print(f"Checking {len(self.movies)} movies")
- 425forstreaminself.movies:
- 426ifre.match(regex,stream.name)isnotNone:
- 427search_result.append(stream.export_json())
- 428
- 429print(f"Checking {len(self.channels)} channels")
- 430forstreaminself.channels:
- 431ifre.match(regex,stream.name)isnotNone:
- 432search_result.append(stream.export_json())
- 433
- 434print(f"Checking {len(self.series)} series")
- 435forstreaminself.series:
- 436ifre.match(regex,stream.name)isnotNone:
- 437search_result.append(stream.export_json())
- 438
- 439ifreturn_type=="JSON":
- 440ifsearch_resultisnotNone:
- 441print(f"Found {len(search_result)} results `{keyword}`")
- 442returnjson.dumps(search_result,ensure_ascii=False)
+ 424search_result=[]
+ 425regex_flags=re.IGNORECASEifignore_caseelse0
+ 426regex=re.compile(keyword,regex_flags)
+ 427# if ignore_case:
+ 428# regex = re.compile(keyword, re.IGNORECASE)
+ 429# else:
+ 430# regex = re.compile(keyword)
+ 431
+ 432# if "movies" in stream_type:
+ 433# print(f"Checking {len(self.movies)} movies")
+ 434# for stream in self.movies:
+ 435# if re.match(regex, stream.name) is not None:
+ 436# search_result.append(stream.export_json())
+ 437
+ 438# if "channels" in stream_type:
+ 439# print(f"Checking {len(self.channels)} channels")
+ 440# for stream in self.channels:
+ 441# if re.match(regex, stream.name) is not None:
+ 442# search_result.append(stream.export_json()) 443
- 444returnsearch_result
- 445
- 446defdownload_video(self,stream_id:int)->str:
- 447"""Download Video from Stream ID
- 448
- 449 Args:
- 450 stream_id (int): Stirng identifing the stream ID
- 451
- 452 Returns:
- 453 str: Absolute Path Filename where the file was saved. Empty if could not download
- 454 """
- 455url=""
- 456filename=""
- 457forstreaminself.movies:
- 458ifstream.id==stream_id:
- 459url=stream.url
- 460fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
- 461filename=osp.join(self.cache_path,fn)
- 462
- 463# If the url was correctly built and file does not exists, start downloading
- 464ifurl!="":
- 465ifnotosp.isfile(filename):
- 466ifnotself._download_video_impl(url,filename):
- 467return"Error"
- 468
- 469returnfilename
+ 444# if "series" in stream_type:
+ 445# print(f"Checking {len(self.series)} series")
+ 446# for stream in self.series:
+ 447# if re.match(regex, stream.name) is not None:
+ 448# search_result.append(stream.export_json())
+ 449
+ 450stream_collections={
+ 451"movies":self.movies,
+ 452"channels":self.channels,
+ 453"series":self.series
+ 454}
+ 455
+ 456forstream_type_nameinstream_type:
+ 457ifstream_type_nameinstream_collections:
+ 458collection=stream_collections[stream_type_name]
+ 459print(f"Checking {len(collection)}{stream_type_name}")
+ 460forstreamincollection:
+ 461ifre.match(regex,stream.name)isnotNone:
+ 462search_result.append(stream.export_json())
+ 463else:
+ 464print(f"`{stream_type_name}` not found in collection")
+ 465
+ 466ifreturn_type=="JSON":
+ 467# if search_result is not None:
+ 468print(f"Found {len(search_result)} results `{keyword}`")
+ 469returnjson.dumps(search_result,ensure_ascii=False) 470
- 471def_download_video_impl(self,url:str,fullpath_filename:str)->bool:
- 472"""Download a stream
- 473
- 474 Args:
- 475 url (str): Complete URL of the stream
- 476 fullpath_filename (str): Complete File path where to save the stream
- 477
- 478 Returns:
- 479 bool: True if successful, False if error
- 480 """
- 481ret_code=False
- 482mb_size=1024*1024
- 483try:
- 484print(f"Downloading from URL `{url}` and saving at `{fullpath_filename}`")
- 485
- 486# Make the request to download
- 487response=requests.get(url,timeout=(5),stream=True,allow_redirects=True,headers=self.connection_headers)
- 488# If there is an answer from the remote server
- 489ifresponse.status_code==200:
- 490# Get content type Binary or Text
- 491content_type=response.headers.get('content-type',None)
- 492
- 493# Get total playlist byte size
- 494total_content_size=int(response.headers.get('content-length',None))
- 495total_content_size_mb=total_content_size/mb_size
- 496
- 497# Set downloaded size
- 498downloaded_bytes=0
- 499
- 500# Set stream blocks
- 501block_bytes=int(4*mb_size)# 4 MB
- 502
- 503print(f"Ready to download {total_content_size_mb:.1f} MB file ({total_content_size})")
- 504ifcontent_type.split('/')[0]!="text":
- 505withopen(fullpath_filename,"wb")asfile:
- 506
- 507# Grab data by block_bytes
- 508fordatainresponse.iter_content(block_bytes,decode_unicode=False):
- 509downloaded_bytes+=block_bytes
- 510progress(downloaded_bytes,total_content_size,"Downloading")
- 511file.write(data)
- 512ifdownloaded_bytes<total_content_size:
- 513print("The file size is incorrect, deleting")
- 514remove(fullpath_filename)
- 515else:
- 516# Set the datatime when it was last retreived
- 517# self.settings.set_
- 518print("")
- 519ret_code=True
- 520else:
- 521print(f"URL has a file with unexpected content-type {content_type}")
- 522else:
- 523print(f"HTTP error {response.status_code} while retrieving from {url}")
- 524exceptExceptionase:
- 525print(e)
- 526
- 527returnret_code
- 528
- 529def_slugify(self,string:str)->str:
- 530"""Normalize string
- 531
- 532 Normalizes string, converts to lowercase, removes non-alpha characters,
- 533 and converts spaces to hyphens.
+ 471returnsearch_result
+ 472
+ 473defdownload_video(self,stream_id:int)->str:
+ 474"""Download Video from Stream ID
+ 475
+ 476 Args:
+ 477 stream_id (int): Stirng identifing the stream ID
+ 478
+ 479 Returns:
+ 480 str: Absolute Path Filename where the file was saved. Empty if could not download
+ 481 """
+ 482url=""
+ 483filename=""
+ 484forstreaminself.movies:
+ 485ifstream.id==stream_id:
+ 486url=stream.url
+ 487fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
+ 488filename=osp.join(self.cache_path,fn)
+ 489
+ 490# If the url was correctly built and file does not exists, start downloading
+ 491ifurl!="":
+ 492#if not osp.isfile(filename):
+ 493ifnotself._download_video_impl(url,filename):
+ 494return"Error"
+ 495
+ 496returnfilename
+ 497
+ 498def_download_video_impl(self,url:str,fullpath_filename:str)->bool:
+ 499"""Download a stream
+ 500
+ 501 Args:
+ 502 url (str): Complete URL of the stream
+ 503 fullpath_filename (str): Complete File path where to save the stream
+ 504
+ 505 Returns:
+ 506 bool: True if successful, False if error
+ 507 """
+ 508ret_code=False
+ 509mb_size=1024*1024
+ 510try:
+ 511print(f"Downloading from URL `{url}` and saving at `{fullpath_filename}`")
+ 512
+ 513# Check if the file already exists
+ 514ifosp.exists(fullpath_filename):
+ 515# If the file exists, resume the download from where it left off
+ 516file_size=osp.getsize(fullpath_filename)
+ 517self.connection_headers['Range']=f'bytes={file_size}-'
+ 518mode='ab'# Append to the existing file
+ 519print(f"Resuming from {file_size:_} bytes")
+ 520else:
+ 521# If the file does not exist, start a new download
+ 522mode='wb'# Write a new file
+ 523
+ 524# Make the request to download
+ 525response=requests.get(url,timeout=(10),stream=True,allow_redirects=True,headers=self.connection_headers)
+ 526# If there is an answer from the remote server
+ 527ifresponse.status_code==200orresponse.status_code==206:
+ 528# Get content type Binary or Text
+ 529content_type=response.headers.get('content-type',None)
+ 530
+ 531# Get total playlist byte size
+ 532total_content_size=int(response.headers.get('content-length',None))
+ 533total_content_size_mb=total_content_size/mb_size 534
- 535 Args:
- 536 string (str): String to be normalized
+ 535# Set downloaded size
+ 536downloaded_bytes=0 537
- 538 Returns:
- 539 str: Normalized String
- 540 """
- 541return"".join(x.lower()forxinstringifx.isprintable())
- 542
- 543def_validate_url(self,url:str)->bool:
- 544regex=re.compile(
- 545r"^(?:http|ftp)s?://"# http:// or https://
- 546r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"# domain...
- 547r"localhost|"# localhost...
- 548r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"# ...or ip
- 549r"(?::\d+)?"# optional port
- 550r"(?:/?|[/?]\S+)$",
- 551re.IGNORECASE,
- 552)
+ 538# Set stream blocks
+ 539block_bytes=int(4*mb_size)# 4 MB
+ 540
+ 541print(f"Ready to download {total_content_size_mb:.1f} MB file ({total_content_size})")
+ 542ifcontent_type.split('/')[0]!="text":
+ 543withopen(fullpath_filename,mode)asfile:
+ 544
+ 545# Grab data by block_bytes
+ 546fordatainresponse.iter_content(block_bytes,decode_unicode=False):
+ 547downloaded_bytes+=block_bytes
+ 548progress(downloaded_bytes,total_content_size,"Downloading")
+ 549file.write(data)
+ 550
+ 551ifdownloaded_bytes==total_content_size:
+ 552ret_code=True 553
- 554returnre.match(regex,url)isnotNone
- 555
- 556def_get_logo_local_path(self,logo_url:str)->str:
- 557"""Convert the Logo URL to a local Logo Path
- 558
- 559 Args:
- 560 logoURL (str): The Logo URL
- 561
- 562 Returns:
- 563 [type]: The logo path as a string or None
- 564 """
- 565local_logo_path=None
- 566iflogo_urlisnotNone:
- 567ifnotself._validate_url(logo_url):
- 568logo_url=None
- 569else:
- 570local_logo_path=osp.join(
- 571self.cache_path,
- 572f"{self._slugify(self.name)}-{self._slugify(osp.split(logo_url)[-1])}"
- 573)
- 574returnlocal_logo_path
- 575
- 576defauthenticate(self):
- 577"""Login to provider"""
- 578# If we have not yet successfully authenticated, attempt authentication
- 579ifself.state["authenticated"]isFalse:
- 580# Erase any previous data
- 581self.auth_data={}
- 582# Loop through 30 seconds
- 583i=0
- 584r=None
- 585# Prepare the authentication url
- 586url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 587print(f"Attempting connection: ",end='')
- 588whilei<30:
- 589try:
- 590# Request authentication, wait 4 seconds maximum
- 591r=requests.get(url,timeout=(4),headers=self.connection_headers)
- 592i=31
- 593exceptrequests.exceptions.ConnectionError:
- 594time.sleep(1)
- 595print(f"{i} ",end='',flush=True)
- 596i+=1
+ 554# Delete Range if it was added
+ 555try:
+ 556delself.connection_headers['Range']
+ 557exceptKeyError:
+ 558pass
+ 559else:
+ 560print(f"URL has a file with unexpected content-type {content_type}")
+ 561else:
+ 562print(f"HTTP error {response.status_code} while retrieving from {url}")
+ 563exceptExceptionase:
+ 564print(e)
+ 565
+ 566returnret_code
+ 567
+ 568def_slugify(self,string:str)->str:
+ 569"""Normalize string
+ 570
+ 571 Normalizes string, converts to lowercase, removes non-alpha characters,
+ 572 and converts spaces to hyphens.
+ 573
+ 574 Args:
+ 575 string (str): String to be normalized
+ 576
+ 577 Returns:
+ 578 str: Normalized String
+ 579 """
+ 580return"".join(x.lower()forxinstringifx.isprintable())
+ 581
+ 582def_validate_url(self,url:str)->bool:
+ 583regex=re.compile(
+ 584r"^(?:http|ftp)s?://"# http:// or https://
+ 585r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"# domain...
+ 586r"localhost|"# localhost...
+ 587r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"# ...or ip
+ 588r"(?::\d+)?"# optional port
+ 589r"(?:/?|[/?]\S+)$",
+ 590re.IGNORECASE,
+ 591)
+ 592
+ 593returnre.match(regex,url)isnotNone
+ 594
+ 595def_get_logo_local_path(self,logo_url:str)->str:
+ 596"""Convert the Logo URL to a local Logo Path 597
- 598ifrisnotNone:
- 599# If the answer is ok, process data and change state
- 600ifr.ok:
- 601self.auth_data=r.json()
- 602self.authorization={
- 603"username":self.auth_data["user_info"]["username"],
- 604"password":self.auth_data["user_info"]["password"]
- 605}
- 606# Mark connection authorized
- 607self.state["authenticated"]=True
- 608# Construct the base url for all requests
- 609self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 610# If there is a secure server connection, construct the base url SSL for all requests
- 611if"https_port"inself.auth_data["server_info"]:
- 612self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
- 613f"/player_api.php?username={self.username}&password={self.password}"
- 614else:
- 615print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
- 616else:
- 617print(f"\n{self.name}: Provider refused the connection")
- 618
- 619def_load_from_file(self,filename)->dict:
- 620"""Try to load the dictionary from file
- 621
- 622 Args:
- 623 filename ([type]): File name containing the data
- 624
- 625 Returns:
- 626 dict: Dictionary if found and no errors, None if file does not exists
- 627 """
- 628# Build the full path
- 629full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 630
- 631# If the cached file exists, attempt to load it
- 632ifosp.isfile(full_filename):
- 633
- 634my_data=None
- 635
- 636# Get the enlapsed seconds since last file update
- 637file_age_sec=time.time()-osp.getmtime(full_filename)
- 638# If the file was updated less than the threshold time,
- 639# it means that the file is still fresh, we can load it.
- 640# Otherwise skip and return None to force a re-download
- 641ifself.threshold_time_sec>file_age_sec:
- 642# Load the JSON data
- 643try:
- 644withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
- 645my_data=json.load(myfile)
- 646iflen(my_data)==0:
- 647my_data=None
- 648exceptExceptionase:
- 649print(f" - Could not load from file `{full_filename}`: e=`{e}`")
- 650returnmy_data
- 651
- 652returnNone
- 653
- 654def_save_to_file(self,data_list:dict,filename:str)->bool:
- 655"""Save a dictionary to file
- 656
- 657 This function will overwrite the file if already exists
- 658
- 659 Args:
- 660 data_list (dict): Dictionary to save
- 661 filename (str): Name of the file
- 662
- 663 Returns:
- 664 bool: True if successfull, False if error
- 665 """
- 666ifdata_listisnotNone:
- 667
- 668#Build the full path
- 669full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 670# If the path makes sense, save the file
- 671json_data=json.dumps(data_list,ensure_ascii=False)
- 672try:
- 673withopen(full_filename,mode="wt",encoding="utf-8")asmyfile:
- 674myfile.write(json_data)
- 675exceptExceptionase:
- 676print(f" - Could not save to file `{full_filename}`: e=`{e}`")
- 677returnFalse
- 678
- 679returnTrue
- 680else:
- 681returnFalse
+ 598 Args:
+ 599 logoURL (str): The Logo URL
+ 600
+ 601 Returns:
+ 602 [type]: The logo path as a string or None
+ 603 """
+ 604local_logo_path=None
+ 605iflogo_urlisnotNone:
+ 606ifnotself._validate_url(logo_url):
+ 607logo_url=None
+ 608else:
+ 609local_logo_path=osp.join(
+ 610self.cache_path,
+ 611f"{self._slugify(self.name)}-{self._slugify(osp.split(logo_url)[-1])}"
+ 612)
+ 613returnlocal_logo_path
+ 614
+ 615defauthenticate(self):
+ 616"""Login to provider"""
+ 617# If we have not yet successfully authenticated, attempt authentication
+ 618ifself.state["authenticated"]isFalse:
+ 619# Erase any previous data
+ 620self.auth_data={}
+ 621# Loop through 30 seconds
+ 622i=0
+ 623r=None
+ 624# Prepare the authentication url
+ 625url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 626print("Attempting connection... ",end='')
+ 627whilei<30:
+ 628try:
+ 629# Request authentication, wait 4 seconds maximum
+ 630r=requests.get(url,timeout=(4),headers=self.connection_headers)
+ 631i=31
+ 632exceptrequests.exceptions.ConnectionError:
+ 633time.sleep(1)
+ 634print(f"{i} ",end='',flush=True)
+ 635i+=1
+ 636
+ 637ifrisnotNone:
+ 638# If the answer is ok, process data and change state
+ 639ifr.ok:
+ 640print("Connected")
+ 641self.auth_data=r.json()
+ 642self.authorization={
+ 643"username":self.auth_data["user_info"]["username"],
+ 644"password":self.auth_data["user_info"]["password"]
+ 645}
+ 646# Account expiration date
+ 647self.account_expiration=timedelta(
+ 648seconds=(
+ 649int(self.auth_data["user_info"]["exp_date"])-datetime.now().timestamp()
+ 650)
+ 651)
+ 652# Mark connection authorized
+ 653self.state["authenticated"]=True
+ 654# Construct the base url for all requests
+ 655self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 656# If there is a secure server connection, construct the base url SSL for all requests
+ 657if"https_port"inself.auth_data["server_info"]:
+ 658self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
+ 659f"/player_api.php?username={self.username}&password={self.password}"
+ 660print(f"Account expires in {str(self.account_expiration)}")
+ 661else:
+ 662print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
+ 663else:
+ 664print(f"\n{self.name}: Provider refused the connection")
+ 665
+ 666def_load_from_file(self,filename)->dict:
+ 667"""Try to load the dictionary from file
+ 668
+ 669 Args:
+ 670 filename ([type]): File name containing the data
+ 671
+ 672 Returns:
+ 673 dict: Dictionary if found and no errors, None if file does not exists
+ 674 """
+ 675# Build the full path
+ 676full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 677
+ 678# If the cached file exists, attempt to load it
+ 679ifosp.isfile(full_filename):
+ 680
+ 681my_data=None 682
- 683defload_iptv(self)->bool:
- 684"""Load XTream IPTV
- 685
- 686 - Add all Live TV to XTream.channels
- 687 - Add all VOD to XTream.movies
- 688 - Add all Series to XTream.series
- 689 Series contains Seasons and Episodes. Those are not automatically
- 690 retrieved from the server to reduce the loading time.
- 691 - Add all groups to XTream.groups
- 692 Groups are for all three channel types, Live TV, VOD, and Series
- 693
- 694 Returns:
- 695 bool: True if successfull, False if error
- 696 """
- 697# If pyxtream has not authenticated the connection, return empty
- 698ifself.state["authenticated"]isFalse:
- 699print("Warning, cannot load steams since authorization failed")
- 700returnFalse
- 701
- 702# If pyxtream has already loaded the data, skip and return success
- 703ifself.state["loaded"]isTrue:
- 704print("Warning, data has already been loaded.")
- 705returnTrue
- 706
- 707forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
- 708## Get GROUPS
+ 683# Get the enlapsed seconds since last file update
+ 684file_age_sec=time.time()-osp.getmtime(full_filename)
+ 685# If the file was updated less than the threshold time,
+ 686# it means that the file is still fresh, we can load it.
+ 687# Otherwise skip and return None to force a re-download
+ 688ifself.threshold_time_sec>file_age_sec:
+ 689# Load the JSON data
+ 690try:
+ 691withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
+ 692my_data=json.load(myfile)
+ 693iflen(my_data)==0:
+ 694my_data=None
+ 695exceptExceptionase:
+ 696print(f" - Could not load from file `{full_filename}`: e=`{e}`")
+ 697returnmy_data
+ 698
+ 699returnNone
+ 700
+ 701def_save_to_file(self,data_list:dict,filename:str)->bool:
+ 702"""Save a dictionary to file
+ 703
+ 704 This function will overwrite the file if already exists
+ 705
+ 706 Args:
+ 707 data_list (dict): Dictionary to save
+ 708 filename (str): Name of the file 709
- 710# Try loading local file
- 711dt=0
- 712start=timer()
- 713all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
- 714# If file empty or does not exists, download it from remote
- 715ifall_catisNone:
- 716# Load all Groups and save file locally
- 717all_cat=self._load_categories_from_provider(loading_stream_type)
- 718ifall_catisnotNone:
- 719self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
- 720dt=timer()-start
- 721
- 722# If we got the GROUPS data, show the statistics and load GROUPS
- 723ifall_catisnotNone:
- 724print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
- 725## Add GROUPS to dictionaries
- 726
- 727# Add the catch-all-errors group
- 728ifloading_stream_type==self.live_type:
- 729self.groups.append(self.live_catch_all_group)
- 730elifloading_stream_type==self.vod_type:
- 731self.groups.append(self.vod_catch_all_group)
- 732elifloading_stream_type==self.series_type:
- 733self.groups.append(self.series_catch_all_group)
- 734
- 735forcat_objinall_cat:
- 736ifschemaValidator(cat_obj,SchemaType.GROUP):
- 737# Create Group (Category)
- 738new_group=Group(cat_obj,loading_stream_type)
- 739# Add to xtream class
- 740self.groups.append(new_group)
- 741else:
- 742# Save what did not pass schema validation
- 743print(cat_obj)
- 744
- 745# Sort Categories
- 746self.groups.sort(key=lambdax:x.name)
- 747else:
- 748print(f" - Could not load {loading_stream_type} Groups")
- 749break
- 750
- 751## Get Streams
- 752
- 753# Try loading local file
- 754dt=0
- 755start=timer()
- 756all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
- 757# If file empty or does not exists, download it from remote
- 758ifall_streamsisNone:
- 759# Load all Streams and save file locally
- 760all_streams=self._load_streams_from_provider(loading_stream_type)
- 761self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
- 762dt=timer()-start
- 763
- 764# If we got the STREAMS data, show the statistics and load Streams
- 765ifall_streamsisnotNone:
- 766print(
- 767f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams " \
- 768f"in {dt:.3f} seconds"
- 769)
- 770## Add Streams to dictionaries
- 771
- 772skipped_adult_content=0
- 773skipped_no_name_content=0
- 774
- 775number_of_streams=len(all_streams)
- 776current_stream_number=0
- 777# Calculate 1% of total number of streams
- 778# This is used to slow down the progress bar
- 779one_percent_number_of_streams=number_of_streams/100
- 780start=timer()
- 781forstream_channelinall_streams:
- 782skip_stream=False
- 783current_stream_number+=1
+ 710 Returns:
+ 711 bool: True if successfull, False if error
+ 712 """
+ 713ifdata_listisNone:
+ 714returnFalse
+ 715
+ 716full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 717try:
+ 718withopen(full_filename,mode="wt",encoding="utf-8")asfile:
+ 719json.dump(data_list,file,ensure_ascii=False)
+ 720returnTrue
+ 721exceptExceptionase:
+ 722print(f" - Could not save to file `{full_filename}`: e=`{e}`")
+ 723returnFalse
+ 724# if data_list is not None:
+ 725
+ 726# #Build the full path
+ 727# full_filename = osp.join(self.cache_path, f"{self._slugify(self.name)}-{filename}")
+ 728# # If the path makes sense, save the file
+ 729# json_data = json.dumps(data_list, ensure_ascii=False)
+ 730# try:
+ 731# with open(full_filename, mode="wt", encoding="utf-8") as myfile:
+ 732# myfile.write(json_data)
+ 733# except Exception as e:
+ 734# print(f" - Could not save to file `{full_filename}`: e=`{e}`")
+ 735# return False
+ 736
+ 737# return True
+ 738# else:
+ 739# return False
+ 740
+ 741defload_iptv(self)->bool:
+ 742"""Load XTream IPTV
+ 743
+ 744 - Add all Live TV to XTream.channels
+ 745 - Add all VOD to XTream.movies
+ 746 - Add all Series to XTream.series
+ 747 Series contains Seasons and Episodes. Those are not automatically
+ 748 retrieved from the server to reduce the loading time.
+ 749 - Add all groups to XTream.groups
+ 750 Groups are for all three channel types, Live TV, VOD, and Series
+ 751
+ 752 Returns:
+ 753 bool: True if successfull, False if error
+ 754 """
+ 755# If pyxtream has not authenticated the connection, return empty
+ 756ifself.state["authenticated"]isFalse:
+ 757print("Warning, cannot load steams since authorization failed")
+ 758returnFalse
+ 759
+ 760# If pyxtream has already loaded the data, skip and return success
+ 761ifself.state["loaded"]isTrue:
+ 762print("Warning, data has already been loaded.")
+ 763returnTrue
+ 764
+ 765forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
+ 766## Get GROUPS
+ 767
+ 768# Try loading local file
+ 769dt=0
+ 770start=timer()
+ 771all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
+ 772# If file empty or does not exists, download it from remote
+ 773ifall_catisNone:
+ 774# Load all Groups and save file locally
+ 775all_cat=self._load_categories_from_provider(loading_stream_type)
+ 776ifall_catisnotNone:
+ 777self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
+ 778dt=timer()-start
+ 779
+ 780# If we got the GROUPS data, show the statistics and load GROUPS
+ 781ifall_catisnotNone:
+ 782print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
+ 783## Add GROUPS to dictionaries 784
- 785# Show download progress every 1% of total number of streams
- 786ifcurrent_stream_number<one_percent_number_of_streams:
- 787progress(
- 788current_stream_number,
- 789number_of_streams,
- 790f"Processing {loading_stream_type} Streams"
- 791)
- 792one_percent_number_of_streams*=2
- 793
- 794# Validate JSON scheme
- 795ifself.validate_json:
- 796ifloading_stream_type==self.series_type:
- 797ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
- 798print(stream_channel)
- 799elifloading_stream_type==self.live_type:
- 800ifnotschemaValidator(stream_channel,SchemaType.LIVE):
- 801print(stream_channel)
- 802else:
- 803# vod_type
- 804ifnotschemaValidator(stream_channel,SchemaType.VOD):
- 805print(stream_channel)
- 806
- 807# Skip if the name of the stream is empty
- 808ifstream_channel["name"]=="":
- 809skip_stream=True
- 810skipped_no_name_content=skipped_no_name_content+1
- 811self._save_to_file_skipped_streams(stream_channel)
- 812
- 813# Skip if the user chose to hide adult streams
- 814ifself.hide_adult_contentandloading_stream_type==self.live_type:
- 815if"is_adult"instream_channel:
- 816ifstream_channel["is_adult"]=="1":
- 817skip_stream=True
- 818skipped_adult_content=skipped_adult_content+1
- 819self._save_to_file_skipped_streams(stream_channel)
- 820
- 821ifnotskip_stream:
- 822# Some channels have no group,
- 823# so let's add them to the catch all group
- 824ifstream_channel["category_id"]isNone:
- 825stream_channel["category_id"]="9999"
- 826elifstream_channel["category_id"]!="1":
- 827pass
- 828
- 829# Find the first occurence of the group that the
- 830# Channel or Stream is pointing to
- 831the_group=next(
- 832(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
- 833None
- 834)
- 835
- 836# Set group title
- 837ifthe_groupisnotNone:
- 838group_title=the_group.name
- 839else:
- 840ifloading_stream_type==self.live_type:
- 841group_title=self.live_catch_all_group.name
- 842the_group=self.live_catch_all_group
- 843elifloading_stream_type==self.vod_type:
- 844group_title=self.vod_catch_all_group.name
- 845the_group=self.vod_catch_all_group
- 846elifloading_stream_type==self.series_type:
- 847group_title=self.series_catch_all_group.name
- 848the_group=self.series_catch_all_group
- 849
- 850
+ 785# Add the catch-all-errors group
+ 786ifloading_stream_type==self.live_type:
+ 787self.groups.append(self.live_catch_all_group)
+ 788elifloading_stream_type==self.vod_type:
+ 789self.groups.append(self.vod_catch_all_group)
+ 790elifloading_stream_type==self.series_type:
+ 791self.groups.append(self.series_catch_all_group)
+ 792
+ 793forcat_objinall_cat:
+ 794ifschemaValidator(cat_obj,SchemaType.GROUP):
+ 795# Create Group (Category)
+ 796new_group=Group(cat_obj,loading_stream_type)
+ 797# Add to xtream class
+ 798self.groups.append(new_group)
+ 799else:
+ 800# Save what did not pass schema validation
+ 801print(cat_obj)
+ 802
+ 803# Sort Categories
+ 804self.groups.sort(key=lambdax:x.name)
+ 805else:
+ 806print(f" - Could not load {loading_stream_type} Groups")
+ 807break
+ 808
+ 809## Get Streams
+ 810
+ 811# Try loading local file
+ 812dt=0
+ 813start=timer()
+ 814all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
+ 815# If file empty or does not exists, download it from remote
+ 816ifall_streamsisNone:
+ 817# Load all Streams and save file locally
+ 818all_streams=self._load_streams_from_provider(loading_stream_type)
+ 819self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
+ 820dt=timer()-start
+ 821
+ 822# If we got the STREAMS data, show the statistics and load Streams
+ 823ifall_streamsisnotNone:
+ 824print(f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams in {dt:.3f} seconds")
+ 825## Add Streams to dictionaries
+ 826
+ 827skipped_adult_content=0
+ 828skipped_no_name_content=0
+ 829
+ 830number_of_streams=len(all_streams)
+ 831current_stream_number=0
+ 832# Calculate 1% of total number of streams
+ 833# This is used to slow down the progress bar
+ 834one_percent_number_of_streams=number_of_streams/100
+ 835start=timer()
+ 836forstream_channelinall_streams:
+ 837skip_stream=False
+ 838current_stream_number+=1
+ 839
+ 840# Show download progress every 1% of total number of streams
+ 841ifcurrent_stream_number<one_percent_number_of_streams:
+ 842progress(
+ 843current_stream_number,
+ 844number_of_streams,
+ 845f"Processing {loading_stream_type} Streams"
+ 846)
+ 847one_percent_number_of_streams*=2
+ 848
+ 849# Validate JSON scheme
+ 850ifself.validate_json: 851ifloading_stream_type==self.series_type:
- 852# Load all Series
- 853new_series=Serie(self,stream_channel)
- 854# To get all the Episodes for every Season of each
- 855# Series is very time consuming, we will only
- 856# populate the Series once the user click on the
- 857# Series, the Seasons and Episodes will be loaded
- 858# using x.getSeriesInfoByID() function
- 859
- 860else:
- 861new_channel=Channel(
- 862self,
- 863group_title,
- 864stream_channel
- 865)
- 866
- 867ifnew_channel.group_id=="9999":
- 868print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
- 869
- 870# Save the new channel to the local list of channels
- 871ifloading_stream_type==self.live_type:
- 872self.channels.append(new_channel)
- 873elifloading_stream_type==self.vod_type:
- 874self.movies.append(new_channel)
- 875ifnew_channel.age_days_from_added<31:
- 876self.movies_30days.append(new_channel)
- 877ifnew_channel.age_days_from_added<7:
- 878self.movies_7days.append(new_channel)
- 879else:
- 880self.series.append(new_series)
- 881
- 882# Add stream to the specific Group
- 883ifthe_groupisnotNone:
- 884ifloading_stream_type!=self.series_type:
- 885the_group.channels.append(new_channel)
- 886else:
- 887the_group.series.append(new_series)
- 888else:
- 889print(f" - Group not found `{stream_channel['name']}`")
- 890print("\n")
- 891# Print information of which streams have been skipped
- 892ifself.hide_adult_content:
- 893print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
- 894ifskipped_no_name_content>0:
- 895print(f" - Skipped {skipped_no_name_content} unprintable {loading_stream_type} streams")
- 896else:
- 897print(f" - Could not load {loading_stream_type} Streams")
- 898
- 899self.state["loaded"]=True
- 900
- 901def_save_to_file_skipped_streams(self,stream_channel:Channel):
- 902
- 903# Build the full path
- 904full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 852ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
+ 853print(stream_channel)
+ 854elifloading_stream_type==self.live_type:
+ 855ifnotschemaValidator(stream_channel,SchemaType.LIVE):
+ 856print(stream_channel)
+ 857else:
+ 858# vod_type
+ 859ifnotschemaValidator(stream_channel,SchemaType.VOD):
+ 860print(stream_channel)
+ 861
+ 862# Skip if the name of the stream is empty
+ 863ifstream_channel["name"]=="":
+ 864skip_stream=True
+ 865skipped_no_name_content=skipped_no_name_content+1
+ 866self._save_to_file_skipped_streams(stream_channel)
+ 867
+ 868# Skip if the user chose to hide adult streams
+ 869ifself.hide_adult_contentandloading_stream_type==self.live_type:
+ 870if"is_adult"instream_channel:
+ 871ifstream_channel["is_adult"]=="1":
+ 872skip_stream=True
+ 873skipped_adult_content=skipped_adult_content+1
+ 874self._save_to_file_skipped_streams(stream_channel)
+ 875
+ 876ifnotskip_stream:
+ 877# Some channels have no group,
+ 878# so let's add them to the catch all group
+ 879ifstream_channel["category_id"]=="":
+ 880stream_channel["category_id"]="9999"
+ 881elifstream_channel["category_id"]!="1":
+ 882pass
+ 883
+ 884# Find the first occurence of the group that the
+ 885# Channel or Stream is pointing to
+ 886the_group=next(
+ 887(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
+ 888None
+ 889)
+ 890
+ 891# Set group title
+ 892ifthe_groupisnotNone:
+ 893group_title=the_group.name
+ 894else:
+ 895ifloading_stream_type==self.live_type:
+ 896group_title=self.live_catch_all_group.name
+ 897the_group=self.live_catch_all_group
+ 898elifloading_stream_type==self.vod_type:
+ 899group_title=self.vod_catch_all_group.name
+ 900the_group=self.vod_catch_all_group
+ 901elifloading_stream_type==self.series_type:
+ 902group_title=self.series_catch_all_group.name
+ 903the_group=self.series_catch_all_group
+ 904 905
- 906# If the path makes sense, save the file
- 907json_data=json.dumps(stream_channel,ensure_ascii=False)
- 908try:
- 909withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
- 910myfile.writelines(json_data)
- 911returnTrue
- 912exceptExceptionase:
- 913print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
- 914returnFalse
- 915
- 916defget_series_info_by_id(self,get_series:dict):
- 917"""Get Seasons and Episodes for a Series
- 918
- 919 Args:
- 920 get_series (dict): Series dictionary
- 921 """
- 922
- 923series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
+ 906ifloading_stream_type==self.series_type:
+ 907# Load all Series
+ 908new_series=Serie(self,stream_channel)
+ 909# To get all the Episodes for every Season of each
+ 910# Series is very time consuming, we will only
+ 911# populate the Series once the user click on the
+ 912# Series, the Seasons and Episodes will be loaded
+ 913# using x.getSeriesInfoByID() function
+ 914
+ 915else:
+ 916new_channel=Channel(
+ 917self,
+ 918group_title,
+ 919stream_channel
+ 920)
+ 921
+ 922ifnew_channel.group_id=="9999":
+ 923print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}") 924
- 925ifseries_seasons["seasons"]isNone:
- 926series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
- 927
- 928forseries_infoinseries_seasons["seasons"]:
- 929season_name=series_info["name"]
- 930season_key=series_info['season_number']
- 931season=Season(season_name)
- 932get_series.seasons[season_name]=season
- 933if"episodes"inseries_seasons.keys():
- 934forseries_seasoninseries_seasons["episodes"].keys():
- 935forepisode_infoinseries_seasons["episodes"][str(series_season)]:
- 936new_episode_channel=Episode(
- 937self,series_info,"Testing",episode_info
- 938)
- 939season.episodes[episode_info["title"]]=new_episode_channel
- 940
- 941def_get_request(self,url:str,timeout:Tuple=(2,15)):
- 942"""Generic GET Request with Error handling
- 943
- 944 Args:
- 945 URL (str): The URL where to GET content
- 946 timeout (Tuple, optional): Connection and Downloading Timeout. Defaults to (2,15).
- 947
- 948 Returns:
- 949 [type]: JSON dictionary of the loaded data, or None
- 950 """
- 951i=0
- 952whilei<10:
- 953time.sleep(1)
- 954try:
- 955r=requests.get(url,timeout=timeout,headers=self.connection_headers)
- 956i=20
- 957ifr.status_code==200:
- 958returnr.json()
- 959exceptrequests.exceptions.ConnectionError:
- 960print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
- 961i+=1
- 962
- 963exceptrequests.exceptions.HTTPError:
- 964print(" - HTTP Error")
- 965i+=1
- 966
- 967exceptrequests.exceptions.TooManyRedirects:
- 968print(" - TooManyRedirects")
- 969i+=1
- 970
- 971exceptrequests.exceptions.ReadTimeout:
- 972print(" - Timeout while loading data")
- 973i+=1
+ 925# Save the new channel to the local list of channels
+ 926ifloading_stream_type==self.live_type:
+ 927self.channels.append(new_channel)
+ 928elifloading_stream_type==self.vod_type:
+ 929self.movies.append(new_channel)
+ 930ifnew_channel.age_days_from_added<31:
+ 931self.movies_30days.append(new_channel)
+ 932ifnew_channel.age_days_from_added<7:
+ 933self.movies_7days.append(new_channel)
+ 934else:
+ 935self.series.append(new_series)
+ 936
+ 937# Add stream to the specific Group
+ 938ifthe_groupisnotNone:
+ 939ifloading_stream_type!=self.series_type:
+ 940the_group.channels.append(new_channel)
+ 941else:
+ 942the_group.series.append(new_series)
+ 943else:
+ 944print(f" - Group not found `{stream_channel['name']}`")
+ 945print("\n")
+ 946# Print information of which streams have been skipped
+ 947ifself.hide_adult_content:
+ 948print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
+ 949ifskipped_no_name_content>0:
+ 950print(f" - Skipped {skipped_no_name_content} "
+ 951"unprintable {loading_stream_type} streams")
+ 952else:
+ 953print(f" - Could not load {loading_stream_type} Streams")
+ 954
+ 955self.state["loaded"]=True
+ 956
+ 957def_save_to_file_skipped_streams(self,stream_channel:Channel):
+ 958
+ 959# Build the full path
+ 960full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 961
+ 962# If the path makes sense, save the file
+ 963json_data=json.dumps(stream_channel,ensure_ascii=False)
+ 964try:
+ 965withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
+ 966myfile.writelines(json_data)
+ 967returnTrue
+ 968exceptExceptionase:
+ 969print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
+ 970returnFalse
+ 971
+ 972defget_series_info_by_id(self,get_series:dict):
+ 973"""Get Seasons and Episodes for a Series 974
- 975returnNone
- 976
- 977# GET Stream Categories
- 978def_load_categories_from_provider(self,stream_type:str):
- 979"""Get from provider all category for specific stream type from provider
+ 975 Args:
+ 976 get_series (dict): Series dictionary
+ 977 """
+ 978
+ 979series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id) 980
- 981 Args:
- 982 stream_type (str): Stream type can be Live, VOD, Series
+ 981ifseries_seasons["seasons"]isNone:
+ 982series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}] 983
- 984 Returns:
- 985 [type]: JSON if successfull, otherwise None
- 986 """
- 987url=""
- 988ifstream_type==self.live_type:
- 989url=self.get_live_categories_URL()
- 990elifstream_type==self.vod_type:
- 991url=self.get_vod_cat_URL()
- 992elifstream_type==self.series_type:
- 993url=self.get_series_cat_URL()
- 994else:
- 995url=""
+ 984forseries_infoinseries_seasons["seasons"]:
+ 985season_name=series_info["name"]
+ 986season_key=series_info['season_number']
+ 987season=Season(season_name)
+ 988get_series.seasons[season_name]=season
+ 989if"episodes"inseries_seasons.keys():
+ 990forseries_seasoninseries_seasons["episodes"].keys():
+ 991forepisode_infoinseries_seasons["episodes"][str(series_season)]:
+ 992new_episode_channel=Episode(
+ 993self,series_info,"Testing",episode_info
+ 994)
+ 995season.episodes[episode_info["title"]]=new_episode_channel 996
- 997returnself._get_request(url)
- 998
- 999# GET Streams
-1000def_load_streams_from_provider(self,stream_type:str):
-1001"""Get from provider all streams for specific stream type
-1002
-1003 Args:
-1004 stream_type (str): Stream type can be Live, VOD, Series
-1005
-1006 Returns:
-1007 [type]: JSON if successfull, otherwise None
-1008 """
-1009url=""
-1010ifstream_type==self.live_type:
-1011url=self.get_live_streams_URL()
-1012elifstream_type==self.vod_type:
-1013url=self.get_vod_streams_URL()
-1014elifstream_type==self.series_type:
-1015url=self.get_series_URL()
-1016else:
-1017url=""
+ 997def_handle_request_exception(self,exception:requests.exceptions.RequestException):
+ 998"""Handle different types of request exceptions."""
+ 999ifisinstance(exception,requests.exceptions.ConnectionError):
+1000print(" - Connection Error: Possible network problem \
+1001 (e.g. DNS failure, refused connection, etc)")
+1002elifisinstance(exception,requests.exceptions.HTTPError):
+1003print(" - HTTP Error")
+1004elifisinstance(exception,requests.exceptions.TooManyRedirects):
+1005print(" - TooManyRedirects")
+1006elifisinstance(exception,requests.exceptions.ReadTimeout):
+1007print(" - Timeout while loading data")
+1008else:
+1009print(f" - An unexpected error occurred: {exception}")
+1010
+1011def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:
+1012"""Generic GET Request with Error handling
+1013
+1014 Args:
+1015 URL (str): The URL where to GET content
+1016 timeout (Tuple[int, int], optional): Connection and Downloading Timeout.
+1017 Defaults to (2,15).1018
-1019returnself._get_request(url)
-1020
-1021# GET Streams by Category
-1022def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
-1023"""Get from provider all streams for specific stream type with category/group ID
-1024
-1025 Args:
-1026 stream_type (str): Stream type can be Live, VOD, Series
-1027 category_id ([type]): Category/Group ID.
-1028
-1029 Returns:
-1030 [type]: JSON if successfull, otherwise None
-1031 """
-1032url=""
-1033
-1034ifstream_type==self.live_type:
-1035url=self.get_live_streams_URL_by_category(category_id)
-1036elifstream_type==self.vod_type:
-1037url=self.get_vod_streams_URL_by_category(category_id)
-1038elifstream_type==self.series_type:
-1039url=self.get_series_URL_by_category(category_id)
-1040else:
-1041url=""
-1042
-1043returnself._get_request(url)
-1044
-1045# GET SERIES Info
-1046def_load_series_info_by_id_from_provider(self,series_id:str):
-1047"""Gets informations about a Serie
-1048
-1049 Args:
-1050 series_id (str): Serie ID as described in Group
+1019 Returns:
+1020 Optional[dict]: JSON dictionary of the loaded data, or None
+1021 """
+1022forattemptinrange(10):
+1023time.sleep(1)
+1024try:
+1025response=requests.get(url,timeout=timeout,headers=self.connection_headers)
+1026response.raise_for_status()# Raise an HTTPError for bad responses (4xx and 5xx)
+1027returnresponse.json()
+1028exceptrequests.exceptions.RequestExceptionase:
+1029self._handle_request_exception(e)
+1030
+1031returnNone
+1032# i = 0
+1033# while i < 10:
+1034# time.sleep(1)
+1035# try:
+1036# r = requests.get(url, timeout=timeout, headers=self.connection_headers)
+1037# i = 20
+1038# if r.status_code == 200:
+1039# return r.json()
+1040# except requests.exceptions.ConnectionError:
+1041# print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
+1042# i += 1
+1043
+1044# except requests.exceptions.HTTPError:
+1045# print(" - HTTP Error")
+1046# i += 1
+1047
+1048# except requests.exceptions.TooManyRedirects:
+1049# print(" - TooManyRedirects")
+1050# i += 11051
-1052 Returns:
-1053 [type]: JSON if successfull, otherwise None
-1054 """
-1055returnself._get_request(self.get_series_info_URL_by_ID(series_id))
-1056
-1057# The seasons array, might be filled or might be completely empty.
-1058# If it is not empty, it will contain the cover, overview and the air date
-1059# of the selected season.
-1060# In your APP if you want to display the series, you have to take that
-1061# from the episodes array.
-1062
-1063# GET VOD Info
-1064defvodInfoByID(self,vod_id):
-1065returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
-1066
-1067# GET short_epg for LIVE Streams (same as stalker portal,
-1068# prints the next X EPG that will play soon)
-1069defliveEpgByStream(self,stream_id):
-1070returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
-1071
-1072defliveEpgByStreamAndLimit(self,stream_id,limit):
-1073returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
-1074
-1075# GET ALL EPG for LIVE Streams (same as stalker portal,
-1076# but it will print all epg listings regardless of the day)
-1077defallLiveEpgByStream(self,stream_id):
-1078returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
+1052# except requests.exceptions.ReadTimeout:
+1053# print(" - Timeout while loading data")
+1054# i += 1
+1055
+1056# return None
+1057
+1058# GET Stream Categories
+1059def_load_categories_from_provider(self,stream_type:str):
+1060"""Get from provider all category for specific stream type from provider
+1061
+1062 Args:
+1063 stream_type (str): Stream type can be Live, VOD, Series
+1064
+1065 Returns:
+1066 [type]: JSON if successfull, otherwise None
+1067 """
+1068url=""
+1069ifstream_type==self.live_type:
+1070url=self.get_live_categories_URL()
+1071elifstream_type==self.vod_type:
+1072url=self.get_vod_cat_URL()
+1073elifstream_type==self.series_type:
+1074url=self.get_series_cat_URL()
+1075else:
+1076url=""
+1077
+1078returnself._get_request(url)1079
-1080# Full EPG List for all Streams
-1081defallEpg(self):
-1082returnself._get_request(self.get_all_epg_URL())
+1080# GET Streams
+1081def_load_streams_from_provider(self,stream_type:str):
+1082"""Get from provider all streams for specific stream type1083
-1084## URL-builder methods
-1085defget_live_categories_URL(self)->str:
-1086returnf"{self.base_url}&action=get_live_categories"
-1087
-1088defget_live_streams_URL(self)->str:
-1089returnf"{self.base_url}&action=get_live_streams"
-1090
-1091defget_live_streams_URL_by_category(self,category_id)->str:
-1092returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
-1093
-1094defget_vod_cat_URL(self)->str:
-1095returnf"{self.base_url}&action=get_vod_categories"
-1096
-1097defget_vod_streams_URL(self)->str:
-1098returnf"{self.base_url}&action=get_vod_streams"
+1084 Args:
+1085 stream_type (str): Stream type can be Live, VOD, Series
+1086
+1087 Returns:
+1088 [type]: JSON if successfull, otherwise None
+1089 """
+1090url=""
+1091ifstream_type==self.live_type:
+1092url=self.get_live_streams_URL()
+1093elifstream_type==self.vod_type:
+1094url=self.get_vod_streams_URL()
+1095elifstream_type==self.series_type:
+1096url=self.get_series_URL()
+1097else:
+1098url=""1099
-1100defget_vod_streams_URL_by_category(self,category_id)->str:
-1101returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
-1102
-1103defget_series_cat_URL(self)->str:
-1104returnf"{self.base_url}&action=get_series_categories"
+1100returnself._get_request(url)
+1101
+1102# GET Streams by Category
+1103def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
+1104"""Get from provider all streams for specific stream type with category/group ID1105
-1106defget_series_URL(self)->str:
-1107returnf"{self.base_url}&action=get_series"
-1108
-1109defget_series_URL_by_category(self,category_id)->str:
-1110returnf"{self.base_url}&action=get_series&category_id={category_id}"
-1111
-1112defget_series_info_URL_by_ID(self,series_id)->str:
-1113returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
+1106 Args:
+1107 stream_type (str): Stream type can be Live, VOD, Series
+1108 category_id ([type]): Category/Group ID.
+1109
+1110 Returns:
+1111 [type]: JSON if successfull, otherwise None
+1112 """
+1113url=""1114
-1115defget_VOD_info_URL_by_ID(self,vod_id)->str:
-1116returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
-1117
-1118defget_live_epg_URL_by_stream(self,stream_id)->str:
-1119returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
-1120
-1121defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
-1122returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
+1115ifstream_type==self.live_type:
+1116url=self.get_live_streams_URL_by_category(category_id)
+1117elifstream_type==self.vod_type:
+1118url=self.get_vod_streams_URL_by_category(category_id)
+1119elifstream_type==self.series_type:
+1120url=self.get_series_URL_by_category(category_id)
+1121else:
+1122url=""1123
-1124defget_all_live_epg_URL_by_stream(self,stream_id)->str:
-1125returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
-1126
-1127defget_all_epg_URL(self)->str:
-1128returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
+1124returnself._get_request(url)
+1125
+1126# GET SERIES Info
+1127def_load_series_info_by_id_from_provider(self,series_id:str):
+1128"""Gets informations about a Serie
+1129
+1130 Args:
+1131 series_id (str): Serie ID as described in Group
+1132
+1133 Returns:
+1134 [type]: JSON if successfull, otherwise None
+1135 """
+1136returnself._get_request(self.get_series_info_URL_by_ID(series_id))
+1137
+1138# The seasons array, might be filled or might be completely empty.
+1139# If it is not empty, it will contain the cover, overview and the air date
+1140# of the selected season.
+1141# In your APP if you want to display the series, you have to take that
+1142# from the episodes array.
+1143
+1144# GET VOD Info
+1145defvodInfoByID(self,vod_id):
+1146returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
+1147
+1148# GET short_epg for LIVE Streams (same as stalker portal,
+1149# prints the next X EPG that will play soon)
+1150defliveEpgByStream(self,stream_id):
+1151returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
+1152
+1153defliveEpgByStreamAndLimit(self,stream_id,limit):
+1154returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
+1155
+1156# GET ALL EPG for LIVE Streams (same as stalker portal,
+1157# but it will print all epg listings regardless of the day)
+1158defallLiveEpgByStream(self,stream_id):
+1159returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
+1160
+1161# Full EPG List for all Streams
+1162defallEpg(self):
+1163returnself._get_request(self.get_all_epg_URL())
+1164
+1165## URL-builder methods
+1166defget_live_categories_URL(self)->str:
+1167returnf"{self.base_url}&action=get_live_categories"
+1168
+1169defget_live_streams_URL(self)->str:
+1170returnf"{self.base_url}&action=get_live_streams"
+1171
+1172defget_live_streams_URL_by_category(self,category_id)->str:
+1173returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
+1174
+1175defget_vod_cat_URL(self)->str:
+1176returnf"{self.base_url}&action=get_vod_categories"
+1177
+1178defget_vod_streams_URL(self)->str:
+1179returnf"{self.base_url}&action=get_vod_streams"
+1180
+1181defget_vod_streams_URL_by_category(self,category_id)->str:
+1182returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
+1183
+1184defget_series_cat_URL(self)->str:
+1185returnf"{self.base_url}&action=get_series_categories"
+1186
+1187defget_series_URL(self)->str:
+1188returnf"{self.base_url}&action=get_series"
+1189
+1190defget_series_URL_by_category(self,category_id)->str:
+1191returnf"{self.base_url}&action=get_series&category_id={category_id}"
+1192
+1193defget_series_info_URL_by_ID(self,series_id)->str:
+1194returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
+1195
+1196defget_VOD_info_URL_by_ID(self,vod_id)->str:
+1197returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
+1198
+1199defget_live_epg_URL_by_stream(self,stream_id)->str:
+1200returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
+1201
+1202defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
+1203returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
+1204
+1205defget_all_live_epg_URL_by_stream(self,stream_id)->str:
+1206returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
+1207
+1208defget_all_epg_URL(self)->str:
+1209returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
@@ -3646,64 +3808,67 @@
343 provider_url (str): URL of the IPTV provider344 headers (dict): Requests Headers345 hide_adult_content(bool, optional): When `True` hide stream that are marked for adult
-346 cache_path (str, optional): Location where to save loaded files. Defaults to empty string.
-347 reload_time_sec (int, optional): Number of seconds before automatic reloading (-1 to turn it OFF)
-348 debug_flask (bool, optional): Enable the debug mode in Flask
-349 validate_json (bool, optional): Check Xtream API provided JSON for validity
-350
-351 Returns: XTream Class Instance
+346 cache_path (str, optional): Location where to save loaded files.
+347 Defaults to empty string.
+348 reload_time_sec (int, optional): Number of seconds before automatic reloading
+349 (-1 to turn it OFF)
+350 debug_flask (bool, optional): Enable the debug mode in Flask
+351 validate_json (bool, optional): Check Xtream API provided JSON for validity352
-353 - Note 1: If it fails to authorize with provided username and password,
-354 auth_data will be an empty dictionary.
-355 - Note 2: The JSON validation option will take considerable amount of time and it should be
-356 used only as a debug tool. The Xtream API JSON from the provider passes through a schema
-357 that represent the best available understanding of how the Xtream API works.
-358 """
-359self.server=provider_url
-360self.username=provider_username
-361self.password=provider_password
-362self.name=provider_name
-363self.cache_path=cache_path
-364self.hide_adult_content=hide_adult_content
-365self.threshold_time_sec=reload_time_sec
-366self.validate_json=validate_json
-367
-368# get the pyxtream local path
-369self.app_fullpath=osp.dirname(osp.realpath(__file__))
+353 Returns: XTream Class Instance
+354
+355 - Note 1: If it fails to authorize with provided username and password,
+356 auth_data will be an empty dictionary.
+357 - Note 2: The JSON validation option will take considerable amount of time and it should be
+358 used only as a debug tool. The Xtream API JSON from the provider passes through a
+359 schema that represent the best available understanding of how the Xtream API
+360 works.
+361 """
+362self.server=provider_url
+363self.username=provider_username
+364self.password=provider_password
+365self.name=provider_name
+366self.cache_path=cache_path
+367self.hide_adult_content=hide_adult_content
+368self.threshold_time_sec=reload_time_sec
+369self.validate_json=validate_json370
-371# prepare location of local html template
-372self.html_template_folder=osp.join(self.app_fullpath,"html")
+371# get the pyxtream local path
+372self.app_fullpath=osp.dirname(osp.realpath(__file__))373
-374# if the cache_path is specified, test that it is a directory
-375ifself.cache_path!="":
-376# If the cache_path is not a directory, clear it
-377ifnotosp.isdir(self.cache_path):
-378print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
-379self.cache_path==""
-380
-381# If the cache_path is still empty, use default
-382ifself.cache_path=="":
-383self.cache_path=osp.expanduser("~/.xtream-cache/")
-384ifnotosp.isdir(self.cache_path):
-385makedirs(self.cache_path,exist_ok=True)
-386print(f"pyxtream cache path located at {self.cache_path}")
-387
-388ifheadersisnotNone:
-389self.connection_headers=headers
-390else:
-391self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
-392
-393self.authenticate()
-394
-395ifself.threshold_time_sec>0:
-396print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
-397else:
-398print("Reload timer is OFF")
-399
-400ifself.state['authenticated']:
-401ifUSE_FLASK:
-402self.flaskapp=FlaskWrap('pyxtream',self,self.html_template_folder,debug=debug_flask)
-403self.flaskapp.start()
+374# prepare location of local html template
+375self.html_template_folder=osp.join(self.app_fullpath,"html")
+376
+377# if the cache_path is specified, test that it is a directory
+378ifself.cache_path!="":
+379# If the cache_path is not a directory, clear it
+380ifnotosp.isdir(self.cache_path):
+381print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
+382self.cache_path==""
+383
+384# If the cache_path is still empty, use default
+385ifself.cache_path=="":
+386self.cache_path=osp.expanduser("~/.xtream-cache/")
+387ifnotosp.isdir(self.cache_path):
+388makedirs(self.cache_path,exist_ok=True)
+389print(f"pyxtream cache path located at {self.cache_path}")
+390
+391ifheadersisnotNone:
+392self.connection_headers=headers
+393else:
+394self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
+395
+396self.authenticate()
+397
+398ifself.threshold_time_sec>0:
+399print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
+400else:
+401print("Reload timer is OFF")
+402
+403ifself.state['authenticated']:
+404ifUSE_FLASK:
+405self.flaskapp=FlaskWrap('pyxtream',self,self.html_template_folder,debug=debug_flask)
+406self.flaskapp.start()
@@ -3716,8 +3881,10 @@
provider_url (str): URL of the IPTV provider
headers (dict): Requests Headers
hide_adult_content(bool, optional): When True hide stream that are marked for adult
- cache_path (str, optional): Location where to save loaded files. Defaults to empty string.
- reload_time_sec (int, optional): Number of seconds before automatic reloading (-1 to turn it OFF)
+ cache_path (str, optional): Location where to save loaded files.
+ Defaults to empty string.
+ reload_time_sec (int, optional): Number of seconds before automatic reloading
+ (-1 to turn it OFF)
debug_flask (bool, optional): Enable the debug mode in Flask
validate_json (bool, optional): Check Xtream API provided JSON for validity
@@ -3727,8 +3894,9 @@
Note 1: If it fails to authorize with provided username and password,
auth_data will be an empty dictionary.
Note 2: The JSON validation option will take considerable amount of time and it should be
-used only as a debug tool. The Xtream API JSON from the provider passes through a schema
-that represent the best available understanding of how the Xtream API works.
+used only as a debug tool. The Xtream API JSON from the provider passes through a
+schema that represent the best available understanding of how the Xtream API
+works.
@@ -4059,52 +4227,76 @@
405defsearch_stream(self,keyword:str,ignore_case:bool=True,return_type:str="LIST")->List:
-406"""Search for streams
-407
-408 Args:
-409 keyword (str): Keyword to search for. Supports REGEX
-410 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
-411 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
-412
-413 Returns:
-414 List: List with all the results, it could be empty. Each result
-415 """
-416
-417search_result=[]
-418
-419ifignore_case:
-420regex=re.compile(keyword,re.IGNORECASE)
-421else:
-422regex=re.compile(keyword)
+
408defsearch_stream(self,keyword:str,
+409ignore_case:bool=True,
+410return_type:str="LIST",
+411stream_type:list=("series","movies","channels"))->list:
+412"""Search for streams
+413
+414 Args:
+415 keyword (str): Keyword to search for. Supports REGEX
+416 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
+417 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
+418 stream_type (list, optional): Search within specific stream type.
+419
+420 Returns:
+421 list: List with all the results, it could be empty.
+422 """423
-424print(f"Checking {len(self.movies)} movies")
-425forstreaminself.movies:
-426ifre.match(regex,stream.name)isnotNone:
-427search_result.append(stream.export_json())
-428
-429print(f"Checking {len(self.channels)} channels")
-430forstreaminself.channels:
-431ifre.match(regex,stream.name)isnotNone:
-432search_result.append(stream.export_json())
-433
-434print(f"Checking {len(self.series)} series")
-435forstreaminself.series:
-436ifre.match(regex,stream.name)isnotNone:
-437search_result.append(stream.export_json())
-438
-439ifreturn_type=="JSON":
-440ifsearch_resultisnotNone:
-441print(f"Found {len(search_result)} results `{keyword}`")
-442returnjson.dumps(search_result,ensure_ascii=False)
+424search_result=[]
+425regex_flags=re.IGNORECASEifignore_caseelse0
+426regex=re.compile(keyword,regex_flags)
+427# if ignore_case:
+428# regex = re.compile(keyword, re.IGNORECASE)
+429# else:
+430# regex = re.compile(keyword)
+431
+432# if "movies" in stream_type:
+433# print(f"Checking {len(self.movies)} movies")
+434# for stream in self.movies:
+435# if re.match(regex, stream.name) is not None:
+436# search_result.append(stream.export_json())
+437
+438# if "channels" in stream_type:
+439# print(f"Checking {len(self.channels)} channels")
+440# for stream in self.channels:
+441# if re.match(regex, stream.name) is not None:
+442# search_result.append(stream.export_json())443
-444returnsearch_result
+444# if "series" in stream_type:
+445# print(f"Checking {len(self.series)} series")
+446# for stream in self.series:
+447# if re.match(regex, stream.name) is not None:
+448# search_result.append(stream.export_json())
+449
+450stream_collections={
+451"movies":self.movies,
+452"channels":self.channels,
+453"series":self.series
+454}
+455
+456forstream_type_nameinstream_type:
+457ifstream_type_nameinstream_collections:
+458collection=stream_collections[stream_type_name]
+459print(f"Checking {len(collection)}{stream_type_name}")
+460forstreamincollection:
+461ifre.match(regex,stream.name)isnotNone:
+462search_result.append(stream.export_json())
+463else:
+464print(f"`{stream_type_name}` not found in collection")
+465
+466ifreturn_type=="JSON":
+467# if search_result is not None:
+468print(f"Found {len(search_result)} results `{keyword}`")
+469returnjson.dumps(search_result,ensure_ascii=False)
+470
+471returnsearch_result
@@ -4113,10 +4305,11 @@
Args:
keyword (str): Keyword to search for. Supports REGEX
ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
- return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
+ return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
+ stream_type (list, optional): Search within specific stream type.
Returns:
- List: List with all the results, it could be empty. Each result
+ list: List with all the results, it could be empty.
@@ -4132,30 +4325,30 @@
-
446defdownload_video(self,stream_id:int)->str:
-447"""Download Video from Stream ID
-448
-449 Args:
-450 stream_id (int): Stirng identifing the stream ID
-451
-452 Returns:
-453 str: Absolute Path Filename where the file was saved. Empty if could not download
-454 """
-455url=""
-456filename=""
-457forstreaminself.movies:
-458ifstream.id==stream_id:
-459url=stream.url
-460fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
-461filename=osp.join(self.cache_path,fn)
-462
-463# If the url was correctly built and file does not exists, start downloading
-464ifurl!="":
-465ifnotosp.isfile(filename):
-466ifnotself._download_video_impl(url,filename):
-467return"Error"
-468
-469returnfilename
+
473defdownload_video(self,stream_id:int)->str:
+474"""Download Video from Stream ID
+475
+476 Args:
+477 stream_id (int): Stirng identifing the stream ID
+478
+479 Returns:
+480 str: Absolute Path Filename where the file was saved. Empty if could not download
+481 """
+482url=""
+483filename=""
+484forstreaminself.movies:
+485ifstream.id==stream_id:
+486url=stream.url
+487fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
+488filename=osp.join(self.cache_path,fn)
+489
+490# If the url was correctly built and file does not exists, start downloading
+491ifurl!="":
+492#if not osp.isfile(filename):
+493ifnotself._download_video_impl(url,filename):
+494return"Error"
+495
+496returnfilename
@@ -4181,48 +4374,56 @@
-
576defauthenticate(self):
-577"""Login to provider"""
-578# If we have not yet successfully authenticated, attempt authentication
-579ifself.state["authenticated"]isFalse:
-580# Erase any previous data
-581self.auth_data={}
-582# Loop through 30 seconds
-583i=0
-584r=None
-585# Prepare the authentication url
-586url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
-587print(f"Attempting connection: ",end='')
-588whilei<30:
-589try:
-590# Request authentication, wait 4 seconds maximum
-591r=requests.get(url,timeout=(4),headers=self.connection_headers)
-592i=31
-593exceptrequests.exceptions.ConnectionError:
-594time.sleep(1)
-595print(f"{i} ",end='',flush=True)
-596i+=1
-597
-598ifrisnotNone:
-599# If the answer is ok, process data and change state
-600ifr.ok:
-601self.auth_data=r.json()
-602self.authorization={
-603"username":self.auth_data["user_info"]["username"],
-604"password":self.auth_data["user_info"]["password"]
-605}
-606# Mark connection authorized
-607self.state["authenticated"]=True
-608# Construct the base url for all requests
-609self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
-610# If there is a secure server connection, construct the base url SSL for all requests
-611if"https_port"inself.auth_data["server_info"]:
-612self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
-613f"/player_api.php?username={self.username}&password={self.password}"
-614else:
-615print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
-616else:
-617print(f"\n{self.name}: Provider refused the connection")
+
615defauthenticate(self):
+616"""Login to provider"""
+617# If we have not yet successfully authenticated, attempt authentication
+618ifself.state["authenticated"]isFalse:
+619# Erase any previous data
+620self.auth_data={}
+621# Loop through 30 seconds
+622i=0
+623r=None
+624# Prepare the authentication url
+625url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+626print("Attempting connection... ",end='')
+627whilei<30:
+628try:
+629# Request authentication, wait 4 seconds maximum
+630r=requests.get(url,timeout=(4),headers=self.connection_headers)
+631i=31
+632exceptrequests.exceptions.ConnectionError:
+633time.sleep(1)
+634print(f"{i} ",end='',flush=True)
+635i+=1
+636
+637ifrisnotNone:
+638# If the answer is ok, process data and change state
+639ifr.ok:
+640print("Connected")
+641self.auth_data=r.json()
+642self.authorization={
+643"username":self.auth_data["user_info"]["username"],
+644"password":self.auth_data["user_info"]["password"]
+645}
+646# Account expiration date
+647self.account_expiration=timedelta(
+648seconds=(
+649int(self.auth_data["user_info"]["exp_date"])-datetime.now().timestamp()
+650)
+651)
+652# Mark connection authorized
+653self.state["authenticated"]=True
+654# Construct the base url for all requests
+655self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+656# If there is a secure server connection, construct the base url SSL for all requests
+657if"https_port"inself.auth_data["server_info"]:
+658self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
+659f"/player_api.php?username={self.username}&password={self.password}"
+660print(f"Account expires in {str(self.account_expiration)}")
+661else:
+662print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
+663else:
+664print(f"\n{self.name}: Provider refused the connection")
@@ -4242,223 +4443,221 @@
-
683defload_iptv(self)->bool:
-684"""Load XTream IPTV
-685
-686 - Add all Live TV to XTream.channels
-687 - Add all VOD to XTream.movies
-688 - Add all Series to XTream.series
-689 Series contains Seasons and Episodes. Those are not automatically
-690 retrieved from the server to reduce the loading time.
-691 - Add all groups to XTream.groups
-692 Groups are for all three channel types, Live TV, VOD, and Series
-693
-694 Returns:
-695 bool: True if successfull, False if error
-696 """
-697# If pyxtream has not authenticated the connection, return empty
-698ifself.state["authenticated"]isFalse:
-699print("Warning, cannot load steams since authorization failed")
-700returnFalse
-701
-702# If pyxtream has already loaded the data, skip and return success
-703ifself.state["loaded"]isTrue:
-704print("Warning, data has already been loaded.")
-705returnTrue
-706
-707forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
-708## Get GROUPS
-709
-710# Try loading local file
-711dt=0
-712start=timer()
-713all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
-714# If file empty or does not exists, download it from remote
-715ifall_catisNone:
-716# Load all Groups and save file locally
-717all_cat=self._load_categories_from_provider(loading_stream_type)
-718ifall_catisnotNone:
-719self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
-720dt=timer()-start
-721
-722# If we got the GROUPS data, show the statistics and load GROUPS
-723ifall_catisnotNone:
-724print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
-725## Add GROUPS to dictionaries
-726
-727# Add the catch-all-errors group
-728ifloading_stream_type==self.live_type:
-729self.groups.append(self.live_catch_all_group)
-730elifloading_stream_type==self.vod_type:
-731self.groups.append(self.vod_catch_all_group)
-732elifloading_stream_type==self.series_type:
-733self.groups.append(self.series_catch_all_group)
-734
-735forcat_objinall_cat:
-736ifschemaValidator(cat_obj,SchemaType.GROUP):
-737# Create Group (Category)
-738new_group=Group(cat_obj,loading_stream_type)
-739# Add to xtream class
-740self.groups.append(new_group)
-741else:
-742# Save what did not pass schema validation
-743print(cat_obj)
-744
-745# Sort Categories
-746self.groups.sort(key=lambdax:x.name)
-747else:
-748print(f" - Could not load {loading_stream_type} Groups")
-749break
-750
-751## Get Streams
-752
-753# Try loading local file
-754dt=0
-755start=timer()
-756all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
-757# If file empty or does not exists, download it from remote
-758ifall_streamsisNone:
-759# Load all Streams and save file locally
-760all_streams=self._load_streams_from_provider(loading_stream_type)
-761self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
-762dt=timer()-start
-763
-764# If we got the STREAMS data, show the statistics and load Streams
-765ifall_streamsisnotNone:
-766print(
-767f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams " \
-768f"in {dt:.3f} seconds"
-769)
-770## Add Streams to dictionaries
-771
-772skipped_adult_content=0
-773skipped_no_name_content=0
-774
-775number_of_streams=len(all_streams)
-776current_stream_number=0
-777# Calculate 1% of total number of streams
-778# This is used to slow down the progress bar
-779one_percent_number_of_streams=number_of_streams/100
-780start=timer()
-781forstream_channelinall_streams:
-782skip_stream=False
-783current_stream_number+=1
+
741defload_iptv(self)->bool:
+742"""Load XTream IPTV
+743
+744 - Add all Live TV to XTream.channels
+745 - Add all VOD to XTream.movies
+746 - Add all Series to XTream.series
+747 Series contains Seasons and Episodes. Those are not automatically
+748 retrieved from the server to reduce the loading time.
+749 - Add all groups to XTream.groups
+750 Groups are for all three channel types, Live TV, VOD, and Series
+751
+752 Returns:
+753 bool: True if successfull, False if error
+754 """
+755# If pyxtream has not authenticated the connection, return empty
+756ifself.state["authenticated"]isFalse:
+757print("Warning, cannot load steams since authorization failed")
+758returnFalse
+759
+760# If pyxtream has already loaded the data, skip and return success
+761ifself.state["loaded"]isTrue:
+762print("Warning, data has already been loaded.")
+763returnTrue
+764
+765forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
+766## Get GROUPS
+767
+768# Try loading local file
+769dt=0
+770start=timer()
+771all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
+772# If file empty or does not exists, download it from remote
+773ifall_catisNone:
+774# Load all Groups and save file locally
+775all_cat=self._load_categories_from_provider(loading_stream_type)
+776ifall_catisnotNone:
+777self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
+778dt=timer()-start
+779
+780# If we got the GROUPS data, show the statistics and load GROUPS
+781ifall_catisnotNone:
+782print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
+783## Add GROUPS to dictionaries784
-785# Show download progress every 1% of total number of streams
-786ifcurrent_stream_number<one_percent_number_of_streams:
-787progress(
-788current_stream_number,
-789number_of_streams,
-790f"Processing {loading_stream_type} Streams"
-791)
-792one_percent_number_of_streams*=2
-793
-794# Validate JSON scheme
-795ifself.validate_json:
-796ifloading_stream_type==self.series_type:
-797ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
-798print(stream_channel)
-799elifloading_stream_type==self.live_type:
-800ifnotschemaValidator(stream_channel,SchemaType.LIVE):
-801print(stream_channel)
-802else:
-803# vod_type
-804ifnotschemaValidator(stream_channel,SchemaType.VOD):
-805print(stream_channel)
-806
-807# Skip if the name of the stream is empty
-808ifstream_channel["name"]=="":
-809skip_stream=True
-810skipped_no_name_content=skipped_no_name_content+1
-811self._save_to_file_skipped_streams(stream_channel)
-812
-813# Skip if the user chose to hide adult streams
-814ifself.hide_adult_contentandloading_stream_type==self.live_type:
-815if"is_adult"instream_channel:
-816ifstream_channel["is_adult"]=="1":
-817skip_stream=True
-818skipped_adult_content=skipped_adult_content+1
-819self._save_to_file_skipped_streams(stream_channel)
-820
-821ifnotskip_stream:
-822# Some channels have no group,
-823# so let's add them to the catch all group
-824ifstream_channel["category_id"]isNone:
-825stream_channel["category_id"]="9999"
-826elifstream_channel["category_id"]!="1":
-827pass
-828
-829# Find the first occurence of the group that the
-830# Channel or Stream is pointing to
-831the_group=next(
-832(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
-833None
-834)
-835
-836# Set group title
-837ifthe_groupisnotNone:
-838group_title=the_group.name
-839else:
-840ifloading_stream_type==self.live_type:
-841group_title=self.live_catch_all_group.name
-842the_group=self.live_catch_all_group
-843elifloading_stream_type==self.vod_type:
-844group_title=self.vod_catch_all_group.name
-845the_group=self.vod_catch_all_group
-846elifloading_stream_type==self.series_type:
-847group_title=self.series_catch_all_group.name
-848the_group=self.series_catch_all_group
-849
-850
+785# Add the catch-all-errors group
+786ifloading_stream_type==self.live_type:
+787self.groups.append(self.live_catch_all_group)
+788elifloading_stream_type==self.vod_type:
+789self.groups.append(self.vod_catch_all_group)
+790elifloading_stream_type==self.series_type:
+791self.groups.append(self.series_catch_all_group)
+792
+793forcat_objinall_cat:
+794ifschemaValidator(cat_obj,SchemaType.GROUP):
+795# Create Group (Category)
+796new_group=Group(cat_obj,loading_stream_type)
+797# Add to xtream class
+798self.groups.append(new_group)
+799else:
+800# Save what did not pass schema validation
+801print(cat_obj)
+802
+803# Sort Categories
+804self.groups.sort(key=lambdax:x.name)
+805else:
+806print(f" - Could not load {loading_stream_type} Groups")
+807break
+808
+809## Get Streams
+810
+811# Try loading local file
+812dt=0
+813start=timer()
+814all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
+815# If file empty or does not exists, download it from remote
+816ifall_streamsisNone:
+817# Load all Streams and save file locally
+818all_streams=self._load_streams_from_provider(loading_stream_type)
+819self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
+820dt=timer()-start
+821
+822# If we got the STREAMS data, show the statistics and load Streams
+823ifall_streamsisnotNone:
+824print(f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams in {dt:.3f} seconds")
+825## Add Streams to dictionaries
+826
+827skipped_adult_content=0
+828skipped_no_name_content=0
+829
+830number_of_streams=len(all_streams)
+831current_stream_number=0
+832# Calculate 1% of total number of streams
+833# This is used to slow down the progress bar
+834one_percent_number_of_streams=number_of_streams/100
+835start=timer()
+836forstream_channelinall_streams:
+837skip_stream=False
+838current_stream_number+=1
+839
+840# Show download progress every 1% of total number of streams
+841ifcurrent_stream_number<one_percent_number_of_streams:
+842progress(
+843current_stream_number,
+844number_of_streams,
+845f"Processing {loading_stream_type} Streams"
+846)
+847one_percent_number_of_streams*=2
+848
+849# Validate JSON scheme
+850ifself.validate_json:851ifloading_stream_type==self.series_type:
-852# Load all Series
-853new_series=Serie(self,stream_channel)
-854# To get all the Episodes for every Season of each
-855# Series is very time consuming, we will only
-856# populate the Series once the user click on the
-857# Series, the Seasons and Episodes will be loaded
-858# using x.getSeriesInfoByID() function
-859
-860else:
-861new_channel=Channel(
-862self,
-863group_title,
-864stream_channel
-865)
-866
-867ifnew_channel.group_id=="9999":
-868print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
-869
-870# Save the new channel to the local list of channels
-871ifloading_stream_type==self.live_type:
-872self.channels.append(new_channel)
-873elifloading_stream_type==self.vod_type:
-874self.movies.append(new_channel)
-875ifnew_channel.age_days_from_added<31:
-876self.movies_30days.append(new_channel)
-877ifnew_channel.age_days_from_added<7:
-878self.movies_7days.append(new_channel)
-879else:
-880self.series.append(new_series)
-881
-882# Add stream to the specific Group
-883ifthe_groupisnotNone:
-884ifloading_stream_type!=self.series_type:
-885the_group.channels.append(new_channel)
-886else:
-887the_group.series.append(new_series)
-888else:
-889print(f" - Group not found `{stream_channel['name']}`")
-890print("\n")
-891# Print information of which streams have been skipped
-892ifself.hide_adult_content:
-893print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
-894ifskipped_no_name_content>0:
-895print(f" - Skipped {skipped_no_name_content} unprintable {loading_stream_type} streams")
-896else:
-897print(f" - Could not load {loading_stream_type} Streams")
-898
-899self.state["loaded"]=True
+852ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
+853print(stream_channel)
+854elifloading_stream_type==self.live_type:
+855ifnotschemaValidator(stream_channel,SchemaType.LIVE):
+856print(stream_channel)
+857else:
+858# vod_type
+859ifnotschemaValidator(stream_channel,SchemaType.VOD):
+860print(stream_channel)
+861
+862# Skip if the name of the stream is empty
+863ifstream_channel["name"]=="":
+864skip_stream=True
+865skipped_no_name_content=skipped_no_name_content+1
+866self._save_to_file_skipped_streams(stream_channel)
+867
+868# Skip if the user chose to hide adult streams
+869ifself.hide_adult_contentandloading_stream_type==self.live_type:
+870if"is_adult"instream_channel:
+871ifstream_channel["is_adult"]=="1":
+872skip_stream=True
+873skipped_adult_content=skipped_adult_content+1
+874self._save_to_file_skipped_streams(stream_channel)
+875
+876ifnotskip_stream:
+877# Some channels have no group,
+878# so let's add them to the catch all group
+879ifstream_channel["category_id"]=="":
+880stream_channel["category_id"]="9999"
+881elifstream_channel["category_id"]!="1":
+882pass
+883
+884# Find the first occurence of the group that the
+885# Channel or Stream is pointing to
+886the_group=next(
+887(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
+888None
+889)
+890
+891# Set group title
+892ifthe_groupisnotNone:
+893group_title=the_group.name
+894else:
+895ifloading_stream_type==self.live_type:
+896group_title=self.live_catch_all_group.name
+897the_group=self.live_catch_all_group
+898elifloading_stream_type==self.vod_type:
+899group_title=self.vod_catch_all_group.name
+900the_group=self.vod_catch_all_group
+901elifloading_stream_type==self.series_type:
+902group_title=self.series_catch_all_group.name
+903the_group=self.series_catch_all_group
+904
+905
+906ifloading_stream_type==self.series_type:
+907# Load all Series
+908new_series=Serie(self,stream_channel)
+909# To get all the Episodes for every Season of each
+910# Series is very time consuming, we will only
+911# populate the Series once the user click on the
+912# Series, the Seasons and Episodes will be loaded
+913# using x.getSeriesInfoByID() function
+914
+915else:
+916new_channel=Channel(
+917self,
+918group_title,
+919stream_channel
+920)
+921
+922ifnew_channel.group_id=="9999":
+923print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+924
+925# Save the new channel to the local list of channels
+926ifloading_stream_type==self.live_type:
+927self.channels.append(new_channel)
+928elifloading_stream_type==self.vod_type:
+929self.movies.append(new_channel)
+930ifnew_channel.age_days_from_added<31:
+931self.movies_30days.append(new_channel)
+932ifnew_channel.age_days_from_added<7:
+933self.movies_7days.append(new_channel)
+934else:
+935self.series.append(new_series)
+936
+937# Add stream to the specific Group
+938ifthe_groupisnotNone:
+939ifloading_stream_type!=self.series_type:
+940the_group.channels.append(new_channel)
+941else:
+942the_group.series.append(new_series)
+943else:
+944print(f" - Group not found `{stream_channel['name']}`")
+945print("\n")
+946# Print information of which streams have been skipped
+947ifself.hide_adult_content:
+948print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
+949ifskipped_no_name_content>0:
+950print(f" - Skipped {skipped_no_name_content} "
+951"unprintable {loading_stream_type} streams")
+952else:
+953print(f" - Could not load {loading_stream_type} Streams")
+954
+955self.state["loaded"]=True
@@ -4491,30 +4690,30 @@
-
916defget_series_info_by_id(self,get_series:dict):
-917"""Get Seasons and Episodes for a Series
-918
-919 Args:
-920 get_series (dict): Series dictionary
-921 """
-922
-923series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
-924
-925ifseries_seasons["seasons"]isNone:
-926series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
-927
-928forseries_infoinseries_seasons["seasons"]:
-929season_name=series_info["name"]
-930season_key=series_info['season_number']
-931season=Season(season_name)
-932get_series.seasons[season_name]=season
-933if"episodes"inseries_seasons.keys():
-934forseries_seasoninseries_seasons["episodes"].keys():
-935forepisode_infoinseries_seasons["episodes"][str(series_season)]:
-936new_episode_channel=Episode(
-937self,series_info,"Testing",episode_info
-938)
-939season.episodes[episode_info["title"]]=new_episode_channel
+
972defget_series_info_by_id(self,get_series:dict):
+973"""Get Seasons and Episodes for a Series
+974
+975 Args:
+976 get_series (dict): Series dictionary
+977 """
+978
+979series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
+980
+981ifseries_seasons["seasons"]isNone:
+982series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
+983
+984forseries_infoinseries_seasons["seasons"]:
+985season_name=series_info["name"]
+986season_key=series_info['season_number']
+987season=Season(season_name)
+988get_series.seasons[season_name]=season
+989if"episodes"inseries_seasons.keys():
+990forseries_seasoninseries_seasons["episodes"].keys():
+991forepisode_infoinseries_seasons["episodes"][str(series_season)]:
+992new_episode_channel=Episode(
+993self,series_info,"Testing",episode_info
+994)
+995season.episodes[episode_info["title"]]=new_episode_channel
54def__init__(self,name,xtream:object,html_template_folder:str=None,host:str="0.0.0.0",port:int=5000,debug:bool=True):
+55
+56log=logging.getLogger('werkzeug')
+57log.setLevel(logging.ERROR)58
-59log=logging.getLogger('werkzeug')
-60log.setLevel(logging.ERROR)
-61
-62self.host=host
-63self.port=port
-64self.debug=debug
-65
-66self.app=Flask(name)
-67self.xt=xtream
-68Thread.__init__(self)
-69
-70# Configure Thread
-71self.name="pyxtream REST API"
-72self.daemon=True
-73
-74# Load HTML Home Template if any
-75ifhtml_template_folderisnotNone:
-76self.home_template_file_name=path.join(html_template_folder,"index.html")
-77ifpath.isfile(self.home_template_file_name):
-78withopen(self.home_template_file_name,'r',encoding="utf-8")ashome_html:
-79self.home_template=home_html.read()
-80
-81# Add all endpoints
-82self.add_endpoint(endpoint='/',endpoint_name='home',handler=[self.home_template,""])
-83self.add_endpoint(endpoint='/stream_search/<term>',endpoint_name='stream_search',handler=[self.xt.search_stream,"stream_search"])
-84self.add_endpoint(endpoint='/download_stream/<stream_id>/',endpoint_name='download_stream',handler=[self.xt.download_video,"download_stream"])
+59self.host=host
+60self.port=port
+61self.debug=debug
+62
+63self.app=Flask(name)
+64self.xt=xtream
+65Thread.__init__(self)
+66
+67# Configure Thread
+68self.name="pyxtream REST API"
+69self.daemon=True
+70
+71# Load HTML Home Template if any
+72ifhtml_template_folderisnotNone:
+73self.home_template_file_name=path.join(html_template_folder,"index.html")
+74ifpath.isfile(self.home_template_file_name):
+75withopen(self.home_template_file_name,'r',encoding="utf-8")ashome_html:
+76self.home_template=home_html.read()
+77
+78# Add all endpoints
+79self.add_endpoint(endpoint='/',endpoint_name='home',handler=[self.home_template,""])
+80self.add_endpoint(endpoint='/stream_search/<term>',endpoint_name='stream_search',handler=[self.xt.search_stream,"stream_search"])
+81self.add_endpoint(endpoint='/download_stream/<stream_id>/',endpoint_name='download_stream',handler=[self.xt.download_video,"download_stream"])
@@ -403,7 +400,7 @@
name is the thread name. By default, a unique name is constructed of
the form "Thread-N" where N is a small decimal number.
-
args is the argument tuple for the target invocation. Defaults to ().
+
args is a list or tuple of arguments for the target invocation. Defaults to ().
kwargs is a dictionary of keyword arguments for the target
invocation. Defaults to {}.
@@ -485,13 +482,27 @@
-
+
+
name
-
+
+
-
+
1181@property
+1182defname(self):
+1183"""A string used for identification purposes only.
+1184
+1185 It has no semantics. Multiple threads may be given the same name. The
+1186 initial name is set by the constructor.
+1187
+1188 """
+1189assertself._initialized,"Thread.__init__() not called"
+1190returnself._name
+
+
+
A string used for identification purposes only.
It has no semantics. Multiple threads may be given the same name. The
@@ -501,13 +512,31 @@
-
+
+
daemon
-
+
+
-
+
1235@property
+1236defdaemon(self):
+1237"""A boolean value indicating whether this thread is a daemon thread.
+1238
+1239 This must be set before start() is called, otherwise RuntimeError is
+1240 raised. Its initial value is inherited from the creating thread; the
+1241 main thread is not a daemon thread and therefore all threads created in
+1242 the main thread default to daemon = False.
+1243
+1244 The entire Python program exits when only daemon threads are left.
+1245
+1246 """
+1247assertself._initialized,"Thread.__init__() not called"
+1248returnself._daemonic
+
+
+
A boolean value indicating whether this thread is a daemon thread.
This must be set before start() is called, otherwise RuntimeError is
@@ -531,8 +560,8 @@
Args:\n provider_name (str): Name of the IPTV provider\n provider_username (str): User name of the IPTV provider\n provider_password (str): Password of the IPTV provider\n provider_url (str): URL of the IPTV provider\n headers (dict): Requests Headers\n hide_adult_content(bool, optional): When True hide stream that are marked for adult\n cache_path (str, optional): Location where to save loaded files. Defaults to empty string.\n reload_time_sec (int, optional): Number of seconds before automatic reloading (-1 to turn it OFF)\n debug_flask (bool, optional): Enable the debug mode in Flask\n validate_json (bool, optional): Check Xtream API provided JSON for validity
\n\n
Returns: XTream Class Instance
\n\n
\n
Note 1: If it fails to authorize with provided username and password,\nauth_data will be an empty dictionary.
\n
Note 2: The JSON validation option will take considerable amount of time and it should be \nused only as a debug tool. The Xtream API JSON from the provider passes through a schema\nthat represent the best available understanding of how the Xtream API works.
Args:\n keyword (str): Keyword to search for. Supports REGEX\n ignore_case (bool, optional): True to ignore case during search. Defaults to \"True\".\n return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to \"LIST\".
\n\n
Returns:\n List: List with all the results, it could be empty. Each result
Add all Series to XTream.series\nSeries contains Seasons and Episodes. Those are not automatically\nretrieved from the server to reduce the loading time.
\n
Add all groups to XTream.groups\nGroups are for all three channel types, Live TV, VOD, and Series
\n
\n\n
Returns:\n bool: True if successfull, False if error
This class can be safely subclassed in a limited fashion. There are two ways\nto specify the activity: by passing a callable object to the constructor, or\nby overriding the run() method in a subclass.
This constructor should always be called with keyword arguments. Arguments are:
\n\n
group should be None; reserved for future extension when a ThreadGroup\nclass is implemented.
\n\n
target is the callable object to be invoked by the run()\nmethod. Defaults to None, meaning nothing is called.
\n\n
name is the thread name. By default, a unique name is constructed of\nthe form \"Thread-N\" where N is a small decimal number.
\n\n
args is the argument tuple for the target invocation. Defaults to ().
\n\n
kwargs is a dictionary of keyword arguments for the target\ninvocation. Defaults to {}.
\n\n
If a subclass overrides the constructor, it must make sure to invoke\nthe base class constructor (Thread.__init__()) before doing anything\nelse to the thread.
A boolean value indicating whether this thread is a daemon thread.
\n\n
This must be set before start() is called, otherwise RuntimeError is\nraised. Its initial value is inherited from the creating thread; the\nmain thread is not a daemon thread and therefore all threads created in\nthe main thread default to daemon = False.
\n\n
The entire Python program exits when only daemon threads are left.
You may override this method in a subclass. The standard run() method\ninvokes the callable object passed to the object's constructor as the\ntarget argument, if any, with sequential and keyword arguments taken\nfrom the args and kwargs arguments, respectively.
Args:\n provider_name (str): Name of the IPTV provider\n provider_username (str): User name of the IPTV provider\n provider_password (str): Password of the IPTV provider\n provider_url (str): URL of the IPTV provider\n headers (dict): Requests Headers\n hide_adult_content(bool, optional): When True hide stream that are marked for adult\n cache_path (str, optional): Location where to save loaded files.\n Defaults to empty string.\n reload_time_sec (int, optional): Number of seconds before automatic reloading\n (-1 to turn it OFF)\n debug_flask (bool, optional): Enable the debug mode in Flask\n validate_json (bool, optional): Check Xtream API provided JSON for validity
\n\n
Returns: XTream Class Instance
\n\n
\n
Note 1: If it fails to authorize with provided username and password,\nauth_data will be an empty dictionary.
\n
Note 2: The JSON validation option will take considerable amount of time and it should be \nused only as a debug tool. The Xtream API JSON from the provider passes through a\nschema that represent the best available understanding of how the Xtream API \nworks.
Args:\n keyword (str): Keyword to search for. Supports REGEX\n ignore_case (bool, optional): True to ignore case during search. Defaults to \"True\".\n return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to \"LIST\".\n stream_type (list, optional): Search within specific stream type.
\n\n
Returns:\n list: List with all the results, it could be empty.
Add all Series to XTream.series\nSeries contains Seasons and Episodes. Those are not automatically\nretrieved from the server to reduce the loading time.
\n
Add all groups to XTream.groups\nGroups are for all three channel types, Live TV, VOD, and Series
\n
\n\n
Returns:\n bool: True if successfull, False if error
This class can be safely subclassed in a limited fashion. There are two ways\nto specify the activity: by passing a callable object to the constructor, or\nby overriding the run() method in a subclass.
This constructor should always be called with keyword arguments. Arguments are:
\n\n
group should be None; reserved for future extension when a ThreadGroup\nclass is implemented.
\n\n
target is the callable object to be invoked by the run()\nmethod. Defaults to None, meaning nothing is called.
\n\n
name is the thread name. By default, a unique name is constructed of\nthe form \"Thread-N\" where N is a small decimal number.
\n\n
args is a list or tuple of arguments for the target invocation. Defaults to ().
\n\n
kwargs is a dictionary of keyword arguments for the target\ninvocation. Defaults to {}.
\n\n
If a subclass overrides the constructor, it must make sure to invoke\nthe base class constructor (Thread.__init__()) before doing anything\nelse to the thread.
A boolean value indicating whether this thread is a daemon thread.
\n\n
This must be set before start() is called, otherwise RuntimeError is\nraised. Its initial value is inherited from the creating thread; the\nmain thread is not a daemon thread and therefore all threads created in\nthe main thread default to daemon = False.
\n\n
The entire Python program exits when only daemon threads are left.
You may override this method in a subclass. The standard run() method\ninvokes the callable object passed to the object's constructor as the\ntarget argument, if any, with sequential and keyword arguments taken\nfrom the args and kwargs arguments, respectively.
17# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT18# OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 19# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-20importsys
+20importsys2122
-23defprogress(count,total,status=''):
+23defprogress(count,total,status=''):24bar_len=6025filled_len=int(round(bar_len*count/float(total)))26
@@ -100,7 +100,7 @@
62# This contains the raw JSON data 63raw="" 64
- 65def__init__(self,xtream:object,group_title,stream_info):
+ 65def__init__(self,xtream:object,group_title,stream_info): 66self.date_now=datetime.now() 67 68stream_type=stream_info["stream_type"]
@@ -556,7 +556,7 @@
138# This contains the raw JSON data 139raw="" 140
- 141defconvert_region_shortname_to_fullname(self,shortname):
+ 141defconvert_region_shortname_to_fullname(self,shortname): 142 143ifshortname=="AR": 144return"Arab"
@@ -592,7 +592,7 @@
153 154return"" 155
- 156def__init__(self,group_info:dict,stream_type:str):
+ 156def__init__(self,group_info:dict,stream_type:str): 157# Raw JSON Group 158self.raw=group_info 159
@@ -623,7 +623,7 @@
195# This contains the raw JSON data 196raw="" 197
- 198def__init__(self,xtream:object,series_info,group_title,episode_info)->None:
+ 198def__init__(self,xtream:object,series_info,group_title,episode_info)->None: 199# Raw JSON Episode 200self.raw=episode_info 201
@@ -658,7 +658,7 @@
234# This contains the raw JSON data 235raw="" 236
- 237def__init__(self,xtream:object,series_info):
+ 237def__init__(self,xtream:object,series_info): 238# Raw JSON Series 239self.raw=series_info 240self.xtream=xtream
@@ -702,7 +702,7 @@
591 592returnre.match(regex,url)isnotNone 593
- 594def_get_logo_local_path(self,logo_url:str)->str:
+ 594def_get_logo_local_path(self,logo_url:str)->str: 595"""Convert the Logo URL to a local Logo Path 596 597 Args:
@@ -1050,7 +1050,7 @@
611) 612returnlocal_logo_path 613
- 614defauthenticate(self):
+ 614defauthenticate(self): 615"""Login to provider""" 616# If we have not yet successfully authenticated, attempt authentication 617ifself.state["authenticated"]isFalse:
@@ -1101,7 +1101,7 @@
662else: 663print(f"\n{self.name}: Provider refused the connection") 664
- 665def_load_from_file(self,filename)->dict:
+ 665def_load_from_file(self,filename)->dict: 666"""Try to load the dictionary from file 667 668 Args:
@@ -1136,7 +1136,7 @@
697 698returnNone 699
- 700def_save_to_file(self,data_list:dict,filename:str)->bool:
+ 700def_save_to_file(self,data_list:dict,filename:str)->bool: 701"""Save a dictionary to file 702 703 This function will overwrite the file if already exists
@@ -1176,7 +1176,7 @@
737# else: 738# return False 739
- 740defload_iptv(self)->bool:
+ 740defload_iptv(self)->bool: 741"""Load XTream IPTV 742 743 - Add all Live TV to XTream.channels
@@ -1392,7 +1392,7 @@
953 954self.state["loaded"]=True 955
- 956def_save_to_file_skipped_streams(self,stream_channel:Channel):
+ 956def_save_to_file_skipped_streams(self,stream_channel:Channel): 957 958# Build the full path 959full_filename=osp.join(self.cache_path,"skipped_streams.json")
@@ -1407,7 +1407,7 @@
968print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`") 969returnFalse 970
- 971defget_series_info_by_id(self,get_series:dict):
+ 971defget_series_info_by_id(self,get_series:dict): 972"""Get Seasons and Episodes for a Series 973 974 Args:
@@ -1432,7 +1432,7 @@
993) 994season.episodes[episode_info["title"]]=new_episode_channel 995
- 996def_handle_request_exception(self,exception:requests.exceptions.RequestException):
+ 996def_handle_request_exception(self,exception:requests.exceptions.RequestException): 997"""Handle different types of request exceptions.""" 998ifisinstance(exception,requests.exceptions.ConnectionError): 999print(" - Connection Error: Possible network problem \
@@ -1446,7 +1446,7 @@
1007else:1008print(f" - An unexpected error occurred: {exception}")1009
-1010def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:
+1010def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:1011"""Generic GET Request with Error handling10121013 Args:
@@ -1494,7 +1494,7 @@
1055# return None10561057# GET Stream Categories
-1058def_load_categories_from_provider(self,stream_type:str):
+1058def_load_categories_from_provider(self,stream_type:str):1059"""Get from provider all category for specific stream type from provider10601061 Args:
@@ -1516,7 +1516,7 @@
1077returnself._get_request(url)10781079# GET Streams
-1080def_load_streams_from_provider(self,stream_type:str):
+1080def_load_streams_from_provider(self,stream_type:str):1081"""Get from provider all streams for specific stream type10821083 Args:
@@ -1538,7 +1538,7 @@
1099returnself._get_request(url)11001101# GET Streams by Category
-1102def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
+1102def_load_streams_by_category_from_provider(self,stream_type:str,category_id):1103"""Get from provider all streams for specific stream type with category/group ID11041105 Args:
@@ -1562,7 +1562,7 @@
1123returnself._get_request(url)11241125# GET SERIES Info
-1126def_load_series_info_by_id_from_provider(self,series_id:str):
+1126def_load_series_info_by_id_from_provider(self,series_id:str):1127"""Gets informations about a Serie11281129 Args:
@@ -1580,70 +1580,70 @@
1141# from the episodes array.11421143# GET VOD Info
-1144defvodInfoByID(self,vod_id):
+1144defvodInfoByID(self,vod_id):1145returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))11461147# GET short_epg for LIVE Streams (same as stalker portal,1148# prints the next X EPG that will play soon)
-1149defliveEpgByStream(self,stream_id):
+1149defliveEpgByStream(self,stream_id):1150returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))1151
-1152defliveEpgByStreamAndLimit(self,stream_id,limit):
+1152defliveEpgByStreamAndLimit(self,stream_id,limit):1153returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))11541155# GET ALL EPG for LIVE Streams (same as stalker portal,1156# but it will print all epg listings regardless of the day)
-1157defallLiveEpgByStream(self,stream_id):
+1157defallLiveEpgByStream(self,stream_id):1158returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))11591160# Full EPG List for all Streams
-1161defallEpg(self):
+1161defallEpg(self):1162returnself._get_request(self.get_all_epg_URL())11631164## URL-builder methods
-1165defget_live_categories_URL(self)->str:
+1165defget_live_categories_URL(self)->str:1166returnf"{self.base_url}&action=get_live_categories"1167
-1168defget_live_streams_URL(self)->str:
+1168defget_live_streams_URL(self)->str:1169returnf"{self.base_url}&action=get_live_streams"1170
-1171defget_live_streams_URL_by_category(self,category_id)->str:
+1171defget_live_streams_URL_by_category(self,category_id)->str:1172returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"1173
-1174defget_vod_cat_URL(self)->str:
+1174defget_vod_cat_URL(self)->str:1175returnf"{self.base_url}&action=get_vod_categories"1176
-1177defget_vod_streams_URL(self)->str:
+1177defget_vod_streams_URL(self)->str:1178returnf"{self.base_url}&action=get_vod_streams"1179
-1180defget_vod_streams_URL_by_category(self,category_id)->str:
+1180defget_vod_streams_URL_by_category(self,category_id)->str:1181returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"1182
-1183defget_series_cat_URL(self)->str:
+1183defget_series_cat_URL(self)->str:1184returnf"{self.base_url}&action=get_series_categories"1185
-1186defget_series_URL(self)->str:
+1186defget_series_URL(self)->str:1187returnf"{self.base_url}&action=get_series"1188
-1189defget_series_URL_by_category(self,category_id)->str:
+1189defget_series_URL_by_category(self,category_id)->str:1190returnf"{self.base_url}&action=get_series&category_id={category_id}"1191
-1192defget_series_info_URL_by_ID(self,series_id)->str:
+1192defget_series_info_URL_by_ID(self,series_id)->str:1193returnf"{self.base_url}&action=get_series_info&series_id={series_id}"1194
-1195defget_VOD_info_URL_by_ID(self,vod_id)->str:
+1195defget_VOD_info_URL_by_ID(self,vod_id)->str:1196returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"1197
-1198defget_live_epg_URL_by_stream(self,stream_id)->str:
+1198defget_live_epg_URL_by_stream(self,stream_id)->str:1199returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"1200
-1201defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
+1201defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:1202returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"1203
-1204defget_all_live_epg_URL_by_stream(self,stream_id)->str:
+1204defget_all_live_epg_URL_by_stream(self,stream_id)->str:1205returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"1206
-1207defget_all_epg_URL(self)->str:
+1207defget_all_epg_URL(self)->str:1208returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
63# This contains the raw JSON data 64raw="" 65
- 66def__init__(self,xtream:object,group_title,stream_info):
+ 66def__init__(self,xtream:object,group_title,stream_info): 67self.date_now=datetime.now() 68 69stream_type=stream_info["stream_type"]
@@ -1738,7 +1738,7 @@
131classGroup:132# Required by Hypnotix133name=""134group_type=""
@@ -2060,7 +2060,7 @@
139# This contains the raw JSON data140raw=""141
-142defconvert_region_shortname_to_fullname(self,shortname):
+142defconvert_region_shortname_to_fullname(self,shortname):143144ifshortname=="AR":145return"Arab"
@@ -2075,7 +2075,7 @@
154155return""156
-157def__init__(self,group_info:dict,stream_type:str):
+157def__init__(self,group_info:dict,stream_type:str):158# Raw JSON Group159self.raw=group_info160
@@ -2119,7 +2119,7 @@
188classEpisode:189# Required by Hypnotix190title=""191name=""
@@ -2300,7 +2300,7 @@
196# This contains the raw JSON data197raw=""198
-199def__init__(self,xtream:object,series_info,group_title,episode_info)->None:
+199def__init__(self,xtream:object,series_info,group_title,episode_info)->None:200# Raw JSON Episode201self.raw=episode_info202
@@ -2337,7 +2337,7 @@
223classSerie:224# Required by Hypnotix225name=""226logo=""
@@ -2528,7 +2528,7 @@
235# This contains the raw JSON data236raw=""237
-238def__init__(self,xtream:object,series_info):
+238def__init__(self,xtream:object,series_info):239# Raw JSON Series240self.raw=series_info241self.xtream=xtream
@@ -2557,7 +2557,7 @@
592 593returnre.match(regex,url)isnotNone 594
- 595def_get_logo_local_path(self,logo_url:str)->str:
+ 595def_get_logo_local_path(self,logo_url:str)->str: 596"""Convert the Logo URL to a local Logo Path 597 598 Args:
@@ -3176,7 +3176,7 @@
612) 613returnlocal_logo_path 614
- 615defauthenticate(self):
+ 615defauthenticate(self): 616"""Login to provider""" 617# If we have not yet successfully authenticated, attempt authentication 618ifself.state["authenticated"]isFalse:
@@ -3227,7 +3227,7 @@
663else: 664print(f"\n{self.name}: Provider refused the connection") 665
- 666def_load_from_file(self,filename)->dict:
+ 666def_load_from_file(self,filename)->dict: 667"""Try to load the dictionary from file 668 669 Args:
@@ -3262,7 +3262,7 @@
698 699returnNone 700
- 701def_save_to_file(self,data_list:dict,filename:str)->bool:
+ 701def_save_to_file(self,data_list:dict,filename:str)->bool: 702"""Save a dictionary to file 703 704 This function will overwrite the file if already exists
@@ -3302,7 +3302,7 @@
738# else: 739# return False 740
- 741defload_iptv(self)->bool:
+ 741defload_iptv(self)->bool: 742"""Load XTream IPTV 743 744 - Add all Live TV to XTream.channels
@@ -3518,7 +3518,7 @@
954 955self.state["loaded"]=True 956
- 957def_save_to_file_skipped_streams(self,stream_channel:Channel):
+ 957def_save_to_file_skipped_streams(self,stream_channel:Channel): 958 959# Build the full path 960full_filename=osp.join(self.cache_path,"skipped_streams.json")
@@ -3533,7 +3533,7 @@
969print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`") 970returnFalse 971
- 972defget_series_info_by_id(self,get_series:dict):
+ 972defget_series_info_by_id(self,get_series:dict): 973"""Get Seasons and Episodes for a Series 974 975 Args:
@@ -3558,7 +3558,7 @@
994) 995season.episodes[episode_info["title"]]=new_episode_channel 996
- 997def_handle_request_exception(self,exception:requests.exceptions.RequestException):
+ 997def_handle_request_exception(self,exception:requests.exceptions.RequestException): 998"""Handle different types of request exceptions.""" 999ifisinstance(exception,requests.exceptions.ConnectionError):1000print(" - Connection Error: Possible network problem \
@@ -3572,7 +3572,7 @@
1008else:1009print(f" - An unexpected error occurred: {exception}")1010
-1011def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:
+1011def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:1012"""Generic GET Request with Error handling10131014 Args:
@@ -3620,7 +3620,7 @@
1056# return None10571058# GET Stream Categories
-1059def_load_categories_from_provider(self,stream_type:str):
+1059def_load_categories_from_provider(self,stream_type:str):1060"""Get from provider all category for specific stream type from provider10611062 Args:
@@ -3642,7 +3642,7 @@
1078returnself._get_request(url)10791080# GET Streams
-1081def_load_streams_from_provider(self,stream_type:str):
+1081def_load_streams_from_provider(self,stream_type:str):1082"""Get from provider all streams for specific stream type10831084 Args:
@@ -3664,7 +3664,7 @@
1100returnself._get_request(url)11011102# GET Streams by Category
-1103def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
+1103def_load_streams_by_category_from_provider(self,stream_type:str,category_id):1104"""Get from provider all streams for specific stream type with category/group ID11051106 Args:
@@ -3688,7 +3688,7 @@
1124returnself._get_request(url)11251126# GET SERIES Info
-1127def_load_series_info_by_id_from_provider(self,series_id:str):
+1127def_load_series_info_by_id_from_provider(self,series_id:str):1128"""Gets informations about a Serie11291130 Args:
@@ -3706,70 +3706,70 @@
1142# from the episodes array.11431144# GET VOD Info
-1145defvodInfoByID(self,vod_id):
+1145defvodInfoByID(self,vod_id):1146returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))11471148# GET short_epg for LIVE Streams (same as stalker portal,1149# prints the next X EPG that will play soon)
-1150defliveEpgByStream(self,stream_id):
+1150defliveEpgByStream(self,stream_id):1151returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))1152
-1153defliveEpgByStreamAndLimit(self,stream_id,limit):
+1153defliveEpgByStreamAndLimit(self,stream_id,limit):1154returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))11551156# GET ALL EPG for LIVE Streams (same as stalker portal,1157# but it will print all epg listings regardless of the day)
-1158defallLiveEpgByStream(self,stream_id):
+1158defallLiveEpgByStream(self,stream_id):1159returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))11601161# Full EPG List for all Streams
-1162defallEpg(self):
+1162defallEpg(self):1163returnself._get_request(self.get_all_epg_URL())11641165## URL-builder methods
-1166defget_live_categories_URL(self)->str:
+1166defget_live_categories_URL(self)->str:1167returnf"{self.base_url}&action=get_live_categories"1168
-1169defget_live_streams_URL(self)->str:
+1169defget_live_streams_URL(self)->str:1170returnf"{self.base_url}&action=get_live_streams"1171
-1172defget_live_streams_URL_by_category(self,category_id)->str:
+1172defget_live_streams_URL_by_category(self,category_id)->str:1173returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"1174
-1175defget_vod_cat_URL(self)->str:
+1175defget_vod_cat_URL(self)->str:1176returnf"{self.base_url}&action=get_vod_categories"1177
-1178defget_vod_streams_URL(self)->str:
+1178defget_vod_streams_URL(self)->str:1179returnf"{self.base_url}&action=get_vod_streams"1180
-1181defget_vod_streams_URL_by_category(self,category_id)->str:
+1181defget_vod_streams_URL_by_category(self,category_id)->str:1182returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"1183
-1184defget_series_cat_URL(self)->str:
+1184defget_series_cat_URL(self)->str:1185returnf"{self.base_url}&action=get_series_categories"1186
-1187defget_series_URL(self)->str:
+1187defget_series_URL(self)->str:1188returnf"{self.base_url}&action=get_series"1189
-1190defget_series_URL_by_category(self,category_id)->str:
+1190defget_series_URL_by_category(self,category_id)->str:1191returnf"{self.base_url}&action=get_series&category_id={category_id}"1192
-1193defget_series_info_URL_by_ID(self,series_id)->str:
+1193defget_series_info_URL_by_ID(self,series_id)->str:1194returnf"{self.base_url}&action=get_series_info&series_id={series_id}"1195
-1196defget_VOD_info_URL_by_ID(self,vod_id)->str:
+1196defget_VOD_info_URL_by_ID(self,vod_id)->str:1197returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"1198
-1199defget_live_epg_URL_by_stream(self,stream_id)->str:
+1199defget_live_epg_URL_by_stream(self,stream_id)->str:1200returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"1201
-1202defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
+1202defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:1203returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"1204
-1205defget_all_live_epg_URL_by_stream(self,stream_id)->str:
+1205defget_all_live_epg_URL_by_stream(self,stream_id)->str:1206returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"1207
-1208defget_all_epg_URL(self)->str:
+1208defget_all_epg_URL(self)->str:1209returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
615defauthenticate(self):616"""Login to provider"""617# If we have not yet successfully authenticated, attempt authentication618ifself.state["authenticated"]isFalse:
@@ -4443,7 +4443,7 @@
54def__init__(self,name,xtream:object,html_template_folder:str=None,host:str="0.0.0.0",port:int=5000,debug:bool=True):
-55
-56log=logging.getLogger('werkzeug')
-57log.setLevel(logging.ERROR)
-58
-59self.host=host
-60self.port=port
-61self.debug=debug
-62
-63self.app=Flask(name)
-64self.xt=xtream
-65Thread.__init__(self)
-66
-67# Configure Thread
-68self.name="pyxtream REST API"
-69self.daemon=True
-70
-71# Load HTML Home Template if any
-72ifhtml_template_folderisnotNone:
-73self.home_template_file_name=path.join(html_template_folder,"index.html")
-74ifpath.isfile(self.home_template_file_name):
-75withopen(self.home_template_file_name,'r',encoding="utf-8")ashome_html:
-76self.home_template=home_html.read()
-77
-78# Add all endpoints
-79self.add_endpoint(endpoint='/',endpoint_name='home',handler=[self.home_template,""])
-80self.add_endpoint(endpoint='/stream_search/<term>',endpoint_name='stream_search',handler=[self.xt.search_stream,"stream_search"])
-81self.add_endpoint(endpoint='/download_stream/<stream_id>/',endpoint_name='download_stream',handler=[self.xt.download_video,"download_stream"])
+
56def__init__(self,name,xtream:object,html_template_folder:str=None,host:str="0.0.0.0",port:int=5000,debug:bool=True):
+57
+58log=logging.getLogger('werkzeug')
+59log.setLevel(logging.ERROR)
+60
+61self.host=host
+62self.port=port
+63self.debug=debug
+64
+65self.app=Flask(name)
+66self.xt=xtream
+67Thread.__init__(self)
+68
+69# Configure Thread
+70self.name="pyxtream REST API"
+71self.daemon=True
+72
+73# Load HTML Home Template if any
+74ifhtml_template_folderisnotNone:
+75self.home_template_file_name=path.join(html_template_folder,"index.html")
+76ifpath.isfile(self.home_template_file_name):
+77withopen(self.home_template_file_name,'r',encoding="utf-8")ashome_html:
+78self.home_template=home_html.read()
+79
+80# Add all endpoints
+81self.add_endpoint(endpoint='/',endpoint_name='home',handler=[self.home_template,""])
+82self.add_endpoint(endpoint='/stream_search/<term>',endpoint_name='stream_search',handler=[self.xt.search_stream,"stream_search"])
+83self.add_endpoint(endpoint='/download_stream/<stream_id>/',endpoint_name='download_stream',handler=[self.xt.download_video,"download_stream"])
@@ -491,7 +497,7 @@
1181@property
-1182defname(self):
+1182defname(self):1183"""A string used for identification purposes only.11841185 It has no semantics. Multiple threads may be given the same name. The
@@ -521,7 +527,7 @@
1235@property
-1236defdaemon(self):
+1236defdaemon(self):1237"""A boolean value indicating whether this thread is a daemon thread.12381239 This must be set before start() is called, otherwise RuntimeError is
@@ -560,8 +566,8 @@
1# The MIT License (MIT) 2# Copyright (c) 2016 Vladimir Ignatev 3#
- 4# Permission is hereby granted, free of charge, to any person obtaining
- 5# a copy of this software and associated documentation files (the "Software"),
- 6# to deal in the Software without restriction, including without limitation
- 7# the rights to use, copy, modify, merge, publish, distribute, sublicense,
- 8# and/or sell copies of the Software, and to permit persons to whom the Software
+ 4# Permission is hereby granted, free of charge, to any person obtaining
+ 5# a copy of this software and associated documentation files (the "Software"),
+ 6# to deal in the Software without restriction, including without limitation
+ 7# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ 8# and/or sell copies of the Software, and to permit persons to whom the Software 9# is furnished to do so, subject to the following conditions:
-10#
-11# The above copyright notice and this permission notice shall be included
+10#
+11# The above copyright notice and this permission notice shall be included12# in all copies or substantial portions of the Software.13#
-14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-15# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+15# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR16# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE17# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
-18# OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
+18# OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE19# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.20importsys21
diff --git a/docs/pyxtream/pyxtream.html b/docs/pyxtream/pyxtream.html
index f2f376e..62682de 100644
--- a/docs/pyxtream/pyxtream.html
+++ b/docs/pyxtream/pyxtream.html
@@ -210,6 +210,9 @@
4 5Module handles downloading xtream data. 6
- 7Part of this content comes from
+ 7Part of this content comes from 8- https://github.com/chazlarson/py-xtream-codes/blob/master/xtream.py 9- https://github.com/linuxmint/hypnotix 10
@@ -458,1193 +479,1246 @@
19# used for URL validation 20importre 21importtime
- 22fromosimportmakedirs
- 23fromosimportpathasosp
- 24fromosimportremove
- 25# Timing xtream json downloads
- 26fromtimeitimportdefault_timerastimer
- 27fromtypingimportList,Tuple,Optional
- 28fromdatetimeimportdatetime,timedelta
- 29importrequests
- 30
- 31frompyxtream.schemaValidatorimportSchemaType,schemaValidator
- 32
- 33try:
- 34frompyxtream.rest_apiimportFlaskWrap
- 35USE_FLASK=True
- 36exceptImportError:
- 37USE_FLASK=False
- 38
- 39frompyxtream.progressimportprogress
- 40
+ 22importsys
+ 23fromosimportmakedirs
+ 24fromosimportpathasosp
+ 25
+ 26# Timing xtream json downloads
+ 27fromtimeitimportdefault_timerastimer
+ 28fromtypingimportTuple,Optional
+ 29fromdatetimeimportdatetime,timedelta
+ 30importrequests
+ 31
+ 32frompyxtream.schemaValidatorimportSchemaType,schemaValidator
+ 33
+ 34try:
+ 35frompyxtream.rest_apiimportFlaskWrap
+ 36USE_FLASK=True
+ 37exceptImportError:
+ 38USE_FLASK=False
+ 39
+ 40frompyxtream.progressimportprogress 41
- 42classChannel:
- 43# Required by Hypnotix
- 44info=""
- 45id=""
- 46name=""# What is the difference between the below name and title?
- 47logo=""
- 48logo_path=""
- 49group_title=""
- 50title=""
- 51url=""
- 52
- 53# XTream
- 54stream_type:str=""
- 55group_id:str=""
- 56is_adult:int=0
- 57added:int=0
- 58epg_channel_id:str=""
- 59age_days_from_added:int=0
- 60date_now:datetime
- 61
- 62# This contains the raw JSON data
- 63raw=""
- 64
- 65def__init__(self,xtream:object,group_title,stream_info):
- 66self.date_now=datetime.now()
- 67
- 68stream_type=stream_info["stream_type"]
- 69# Adjust the odd "created_live" type
- 70ifstream_typein("created_live","radio_streams"):
- 71stream_type="live"
- 72
- 73ifstream_typenotin("live","movie"):
- 74print(f"Error the channel has unknown stream type `{stream_type}`\n`{stream_info}`")
- 75else:
- 76# Raw JSON Channel
- 77self.raw=stream_info
- 78
- 79stream_name=stream_info["name"]
- 80
- 81# Required by Hypnotix
- 82self.id=stream_info["stream_id"]
- 83self.name=stream_name
- 84self.logo=stream_info["stream_icon"]
- 85self.logo_path=xtream._get_logo_local_path(self.logo)
- 86self.group_title=group_title
- 87self.title=stream_name
- 88
- 89# Check if category_id key is available
- 90if"category_id"instream_info.keys():
- 91self.group_id=int(stream_info["category_id"])
- 92
- 93ifstream_type=="live":
- 94stream_extension="ts"
+ 42
+ 43classChannel:
+ 44# Required by Hypnotix
+ 45info=""
+ 46id=""
+ 47name=""# What is the difference between the below name and title?
+ 48logo=""
+ 49logo_path=""
+ 50group_title=""
+ 51title=""
+ 52url=""
+ 53
+ 54# XTream
+ 55stream_type:str=""
+ 56group_id:str=""
+ 57is_adult:int=0
+ 58added:int=0
+ 59epg_channel_id:str=""
+ 60age_days_from_added:int=0
+ 61date_now:datetime
+ 62
+ 63# This contains the raw JSON data
+ 64raw=""
+ 65
+ 66def__init__(self,xtream:object,group_title,stream_info):
+ 67self.date_now=datetime.now()
+ 68
+ 69stream_type=stream_info["stream_type"]
+ 70# Adjust the odd "created_live" type
+ 71ifstream_typein("created_live","radio_streams"):
+ 72stream_type="live"
+ 73
+ 74ifstream_typenotin("live","movie"):
+ 75print(f"Error the channel has unknown stream type `{stream_type}`\n`{stream_info}`")
+ 76else:
+ 77# Raw JSON Channel
+ 78self.raw=stream_info
+ 79
+ 80stream_name=stream_info["name"]
+ 81
+ 82# Required by Hypnotix
+ 83self.id=stream_info["stream_id"]
+ 84self.name=stream_name
+ 85self.logo=stream_info["stream_icon"]
+ 86self.logo_path=xtream._get_logo_local_path(self.logo)
+ 87self.group_title=group_title
+ 88self.title=stream_name
+ 89
+ 90# Check if category_id key is available
+ 91if"category_id"instream_info.keys():
+ 92self.group_id=int(stream_info["category_id"])
+ 93
+ 94stream_extension="" 95
- 96# Check if epg_channel_id key is available
- 97if"epg_channel_id"instream_info.keys():
- 98self.epg_channel_id=stream_info["epg_channel_id"]
- 99
- 100elifstream_type=="movie":
- 101stream_extension=stream_info["container_extension"]
+ 96ifstream_type=="live":
+ 97stream_extension="ts"
+ 98
+ 99# Check if epg_channel_id key is available
+ 100if"epg_channel_id"instream_info.keys():
+ 101self.epg_channel_id=stream_info["epg_channel_id"] 102
- 103# Default to 0
- 104self.is_adult=0
- 105# Check if is_adult key is available
- 106if"is_adult"instream_info.keys():
- 107self.is_adult=int(stream_info["is_adult"])
- 108
- 109self.added=int(stream_info["added"])
- 110self.age_days_from_added=abs(datetime.utcfromtimestamp(self.added)-self.date_now).days
+ 103elifstream_type=="movie":
+ 104stream_extension=stream_info["container_extension"]
+ 105
+ 106# Default to 0
+ 107self.is_adult=0
+ 108# Check if is_adult key is available
+ 109if"is_adult"instream_info.keys():
+ 110self.is_adult=int(stream_info["is_adult"]) 111
- 112# Required by Hypnotix
- 113self.url=f"{xtream.server}/{stream_type}/{xtream.authorization['username']}/" \
- 114f"{xtream.authorization['password']}/{stream_info['stream_id']}.{stream_extension}"
- 115
- 116# Check that the constructed URL is valid
- 117ifnotxtream._validate_url(self.url):
- 118print(f"{self.name} - Bad URL? `{self.url}`")
- 119
- 120defexport_json(self):
- 121jsondata={}
- 122
- 123jsondata["url"]=self.url
- 124jsondata.update(self.raw)
- 125jsondata["logo_path"]=self.logo_path
- 126
- 127returnjsondata
- 128
- 129
- 130classGroup:
- 131# Required by Hypnotix
- 132name=""
- 133group_type=""
+ 112self.added=int(stream_info["added"])
+ 113self.age_days_from_added=abs(
+ 114datetime.utcfromtimestamp(self.added)-self.date_now
+ 115).days
+ 116
+ 117# Required by Hypnotix
+ 118self.url=f"{xtream.server}/{stream_type}/{xtream.authorization['username']}/" \
+ 119f"{xtream.authorization['password']}/{stream_info['stream_id']}.{stream_extension}"
+ 120
+ 121# Check that the constructed URL is valid
+ 122ifnotxtream._validate_url(self.url):
+ 123print(f"{self.name} - Bad URL? `{self.url}`")
+ 124
+ 125defexport_json(self):
+ 126jsondata={}
+ 127
+ 128jsondata["url"]=self.url
+ 129jsondata.update(self.raw)
+ 130jsondata["logo_path"]=self.logo_path
+ 131
+ 132returnjsondata
+ 133 134
- 135# XTream
- 136group_id=""
- 137
- 138# This contains the raw JSON data
- 139raw=""
- 140
- 141defconvert_region_shortname_to_fullname(self,shortname):
+ 135classGroup:
+ 136# Required by Hypnotix
+ 137name=""
+ 138group_type=""
+ 139
+ 140# XTream
+ 141group_id="" 142
- 143ifshortname=="AR":
- 144return"Arab"
- 145ifshortname=="AM":
- 146return"America"
- 147ifshortname=="AS":
- 148return"Asia"
- 149ifshortname=="AF":
- 150return"Africa"
- 151ifshortname=="EU":
- 152return"Europe"
- 153
- 154return""
- 155
- 156def__init__(self,group_info:dict,stream_type:str):
- 157# Raw JSON Group
- 158self.raw=group_info
- 159
- 160self.channels=[]
- 161self.series=[]
- 162
- 163TV_GROUP,MOVIES_GROUP,SERIES_GROUP=range(3)
+ 143# This contains the raw JSON data
+ 144raw=""
+ 145
+ 146defconvert_region_shortname_to_fullname(self,shortname):
+ 147
+ 148ifshortname=="AR":
+ 149return"Arab"
+ 150ifshortname=="AM":
+ 151return"America"
+ 152ifshortname=="AS":
+ 153return"Asia"
+ 154ifshortname=="AF":
+ 155return"Africa"
+ 156ifshortname=="EU":
+ 157return"Europe"
+ 158
+ 159return""
+ 160
+ 161def__init__(self,group_info:dict,stream_type:str):
+ 162# Raw JSON Group
+ 163self.raw=group_info 164
- 165if"VOD"==stream_type:
- 166self.group_type=MOVIES_GROUP
- 167elif"Series"==stream_type:
- 168self.group_type=SERIES_GROUP
- 169elif"Live"==stream_type:
- 170self.group_type=TV_GROUP
- 171else:
- 172print(f"Unrecognized stream type `{stream_type}` for `{group_info}`")
- 173
- 174self.name=group_info["category_name"]
- 175split_name=self.name.split('|')
- 176self.region_shortname=""
- 177self.region_longname=""
- 178iflen(split_name)>1:
- 179self.region_shortname=split_name[0].strip()
- 180self.region_longname=self.convert_region_shortname_to_fullname(self.region_shortname)
- 181
- 182# Check if category_id key is available
- 183if"category_id"ingroup_info.keys():
- 184self.group_id=int(group_info["category_id"])
- 185
+ 165self.channels=[]
+ 166self.series=[]
+ 167
+ 168TV_GROUP,MOVIES_GROUP,SERIES_GROUP=range(3)
+ 169
+ 170if"VOD"==stream_type:
+ 171self.group_type=MOVIES_GROUP
+ 172elif"Series"==stream_type:
+ 173self.group_type=SERIES_GROUP
+ 174elif"Live"==stream_type:
+ 175self.group_type=TV_GROUP
+ 176else:
+ 177print(f"Unrecognized stream type `{stream_type}` for `{group_info}`")
+ 178
+ 179self.name=group_info["category_name"]
+ 180split_name=self.name.split('|')
+ 181self.region_shortname=""
+ 182self.region_longname=""
+ 183iflen(split_name)>1:
+ 184self.region_shortname=split_name[0].strip()
+ 185self.region_longname=self.convert_region_shortname_to_fullname(self.region_shortname) 186
- 187classEpisode:
- 188# Required by Hypnotix
- 189title=""
- 190name=""
- 191info=""
- 192
- 193# XTream
- 194
- 195# This contains the raw JSON data
- 196raw=""
+ 187# Check if category_id key is available
+ 188if"category_id"ingroup_info.keys():
+ 189self.group_id=int(group_info["category_id"])
+ 190
+ 191
+ 192classEpisode:
+ 193# Required by Hypnotix
+ 194title=""
+ 195name=""
+ 196info="" 197
- 198def__init__(self,xtream:object,series_info,group_title,episode_info)->None:
- 199# Raw JSON Episode
- 200self.raw=episode_info
- 201
- 202self.title=episode_info["title"]
- 203self.name=self.title
- 204self.group_title=group_title
- 205self.id=episode_info["id"]
- 206self.container_extension=episode_info["container_extension"]
- 207self.episode_number=episode_info["episode_num"]
- 208self.av_info=episode_info["info"]
- 209
- 210self.logo=series_info["cover"]
- 211self.logo_path=xtream._get_logo_local_path(self.logo)
- 212
- 213self.url=f"{xtream.server}/series/" \
- 214f"{xtream.authorization['username']}/" \
- 215f"{xtream.authorization['password']}/{self.id}.{self.container_extension}"
- 216
- 217# Check that the constructed URL is valid
- 218ifnotxtream._validate_url(self.url):
- 219print(f"{self.name} - Bad URL? `{self.url}`")
- 220
+ 198# XTream
+ 199
+ 200# This contains the raw JSON data
+ 201raw=""
+ 202
+ 203def__init__(self,xtream:object,series_info,group_title,episode_info)->None:
+ 204# Raw JSON Episode
+ 205self.raw=episode_info
+ 206
+ 207self.title=episode_info["title"]
+ 208self.name=self.title
+ 209self.group_title=group_title
+ 210self.id=episode_info["id"]
+ 211self.container_extension=episode_info["container_extension"]
+ 212self.episode_number=episode_info["episode_num"]
+ 213self.av_info=episode_info["info"]
+ 214
+ 215self.logo=series_info["cover"]
+ 216self.logo_path=xtream._get_logo_local_path(self.logo)
+ 217
+ 218self.url=f"{xtream.server}/series/" \
+ 219f"{xtream.authorization['username']}/" \
+ 220f"{xtream.authorization['password']}/{self.id}.{self.container_extension}" 221
- 222classSerie:
- 223# Required by Hypnotix
- 224name=""
- 225logo=""
- 226logo_path=""
- 227
- 228# XTream
- 229series_id=""
- 230plot=""
- 231youtube_trailer=""
- 232genre=""
- 233
- 234# This contains the raw JSON data
- 235raw=""
- 236
- 237def__init__(self,xtream:object,series_info):
- 238# Raw JSON Series
- 239self.raw=series_info
- 240self.xtream=xtream
+ 222# Check that the constructed URL is valid
+ 223ifnotxtream._validate_url(self.url):
+ 224print(f"{self.name} - Bad URL? `{self.url}`")
+ 225
+ 226
+ 227classSerie:
+ 228# Required by Hypnotix
+ 229name=""
+ 230logo=""
+ 231logo_path=""
+ 232
+ 233# XTream
+ 234series_id=""
+ 235plot=""
+ 236youtube_trailer=""
+ 237genre=""
+ 238
+ 239# This contains the raw JSON data
+ 240raw="" 241
- 242# Required by Hypnotix
- 243self.name=series_info["name"]
- 244self.logo=series_info["cover"]
- 245self.logo_path=xtream._get_logo_local_path(self.logo)
- 246
- 247self.seasons={}
- 248self.episodes={}
+ 242def__init__(self,xtream:object,series_info):
+ 243
+ 244series_info["added"]=series_info["last_modified"]
+ 245
+ 246# Raw JSON Series
+ 247self.raw=series_info
+ 248self.xtream=xtream 249
- 250# Check if category_id key is available
- 251if"series_id"inseries_info.keys():
- 252self.series_id=int(series_info["series_id"])
- 253
- 254# Check if plot key is available
- 255if"plot"inseries_info.keys():
- 256self.plot=series_info["plot"]
+ 250# Required by Hypnotix
+ 251self.name=series_info["name"]
+ 252self.logo=series_info["cover"]
+ 253self.logo_path=xtream._get_logo_local_path(self.logo)
+ 254
+ 255self.seasons={}
+ 256self.episodes={} 257
- 258# Check if youtube_trailer key is available
- 259if"youtube_trailer"inseries_info.keys():
- 260self.youtube_trailer=series_info["youtube_trailer"]
+ 258# Check if category_id key is available
+ 259if"series_id"inseries_info.keys():
+ 260self.series_id=int(series_info["series_id"]) 261
- 262# Check if genre key is available
- 263if"genre"inseries_info.keys():
- 264self.genre=series_info["genre"]
+ 262# Check if plot key is available
+ 263if"plot"inseries_info.keys():
+ 264self.plot=series_info["plot"] 265
- 266defexport_json(self):
- 267jsondata={}
- 268
- 269jsondata.update(self.raw)
- 270jsondata['logo_path']=self.logo_path
- 271
- 272returnjsondata
+ 266# Check if youtube_trailer key is available
+ 267if"youtube_trailer"inseries_info.keys():
+ 268self.youtube_trailer=series_info["youtube_trailer"]
+ 269
+ 270# Check if genre key is available
+ 271if"genre"inseries_info.keys():
+ 272self.genre=series_info["genre"] 273
- 274classSeason:
- 275# Required by Hypnotix
- 276name=""
+ 274self.url=f"{xtream.server}/series/" \
+ 275f"{xtream.authorization['username']}/" \
+ 276f"{xtream.authorization['password']}/{self.series_id}/" 277
- 278def__init__(self,name):
- 279self.name=name
- 280self.episodes={}
- 281
- 282classXTream:
+ 278defexport_json(self):
+ 279jsondata={}
+ 280
+ 281jsondata.update(self.raw)
+ 282jsondata['logo_path']=self.logo_path 283
- 284name=""
- 285server=""
- 286secure_server=""
- 287username=""
- 288password=""
- 289
- 290live_type="Live"
- 291vod_type="VOD"
- 292series_type="Series"
- 293
- 294auth_data={}
- 295authorization={}
- 296
- 297groups=[]
- 298channels=[]
- 299series=[]
- 300movies=[]
- 301movies_30days=[]
- 302movies_7days=[]
- 303
- 304connection_headers={}
+ 284returnjsondata
+ 285
+ 286
+ 287classSeason:
+ 288# Required by Hypnotix
+ 289name=""
+ 290
+ 291def__init__(self,name):
+ 292self.name=name
+ 293self.episodes={}
+ 294
+ 295
+ 296classXTream:
+ 297
+ 298name=""
+ 299server=""
+ 300secure_server=""
+ 301username=""
+ 302password=""
+ 303base_url=""
+ 304base_url_ssl="" 305
- 306state={'authenticated':False,'loaded':False}
+ 306cache_path="" 307
- 308hide_adult_content=False
+ 308account_expiration:timedelta 309
- 310live_catch_all_group=Group(
- 311{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},live_type
- 312)
- 313vod_catch_all_group=Group(
- 314{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},vod_type
- 315)
- 316series_catch_all_group=Group(
- 317{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},series_type
- 318)
- 319# If the cached JSON file is older than threshold_time_sec then load a new
- 320# JSON dictionary from the provider
- 321threshold_time_sec=-1
- 322
- 323def__init__(
- 324self,
- 325provider_name:str,
- 326provider_username:str,
- 327provider_password:str,
- 328provider_url:str,
- 329headers:dict=None,
- 330hide_adult_content:bool=False,
- 331cache_path:str="",
- 332reload_time_sec:int=60*60*8,
- 333validate_json:bool=False,
- 334debug_flask:bool=True
- 335):
- 336"""Initialize Xtream Class
- 337
- 338 Args:
- 339 provider_name (str): Name of the IPTV provider
- 340 provider_username (str): User name of the IPTV provider
- 341 provider_password (str): Password of the IPTV provider
- 342 provider_url (str): URL of the IPTV provider
- 343 headers (dict): Requests Headers
- 344 hide_adult_content(bool, optional): When `True` hide stream that are marked for adult
- 345 cache_path (str, optional): Location where to save loaded files.
- 346 Defaults to empty string.
- 347 reload_time_sec (int, optional): Number of seconds before automatic reloading
- 348 (-1 to turn it OFF)
- 349 debug_flask (bool, optional): Enable the debug mode in Flask
- 350 validate_json (bool, optional): Check Xtream API provided JSON for validity
- 351
- 352 Returns: XTream Class Instance
- 353
- 354 - Note 1: If it fails to authorize with provided username and password,
- 355 auth_data will be an empty dictionary.
- 356 - Note 2: The JSON validation option will take considerable amount of time and it should be
- 357 used only as a debug tool. The Xtream API JSON from the provider passes through a
- 358 schema that represent the best available understanding of how the Xtream API
- 359 works.
- 360 """
- 361self.server=provider_url
- 362self.username=provider_username
- 363self.password=provider_password
- 364self.name=provider_name
- 365self.cache_path=cache_path
- 366self.hide_adult_content=hide_adult_content
- 367self.threshold_time_sec=reload_time_sec
- 368self.validate_json=validate_json
- 369
- 370# get the pyxtream local path
- 371self.app_fullpath=osp.dirname(osp.realpath(__file__))
- 372
- 373# prepare location of local html template
- 374self.html_template_folder=osp.join(self.app_fullpath,"html")
- 375
- 376# if the cache_path is specified, test that it is a directory
- 377ifself.cache_path!="":
- 378# If the cache_path is not a directory, clear it
- 379ifnotosp.isdir(self.cache_path):
- 380print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
- 381self.cache_path==""
- 382
- 383# If the cache_path is still empty, use default
- 384ifself.cache_path=="":
- 385self.cache_path=osp.expanduser("~/.xtream-cache/")
- 386ifnotosp.isdir(self.cache_path):
- 387makedirs(self.cache_path,exist_ok=True)
- 388print(f"pyxtream cache path located at {self.cache_path}")
- 389
- 390ifheadersisnotNone:
- 391self.connection_headers=headers
- 392else:
- 393self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
- 394
- 395self.authenticate()
+ 310live_type="Live"
+ 311vod_type="VOD"
+ 312series_type="Series"
+ 313
+ 314auth_data={}
+ 315authorization={'username':'','password':''}
+ 316
+ 317groups=[]
+ 318channels=[]
+ 319series=[]
+ 320movies=[]
+ 321movies_30days=[]
+ 322movies_7days=[]
+ 323
+ 324connection_headers={}
+ 325
+ 326state={'authenticated':False,'loaded':False}
+ 327
+ 328hide_adult_content=False
+ 329
+ 330live_catch_all_group=Group(
+ 331{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},live_type
+ 332)
+ 333vod_catch_all_group=Group(
+ 334{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},vod_type
+ 335)
+ 336series_catch_all_group=Group(
+ 337{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},series_type
+ 338)
+ 339# If the cached JSON file is older than threshold_time_sec then load a new
+ 340# JSON dictionary from the provider
+ 341threshold_time_sec=-1
+ 342
+ 343validate_json:bool=True
+ 344
+ 345# Used by REST API to get download progress
+ 346download_progress:dict={'StreamId':0,'Total':0,'Progress':0}
+ 347
+ 348def__init__(
+ 349self,
+ 350provider_name:str,
+ 351provider_username:str,
+ 352provider_password:str,
+ 353provider_url:str,
+ 354headers:dict=None,
+ 355hide_adult_content:bool=False,
+ 356cache_path:str="",
+ 357reload_time_sec:int=60*60*8,
+ 358validate_json:bool=False,
+ 359enable_flask:bool=False,
+ 360debug_flask:bool=True
+ 361):
+ 362"""Initialize Xtream Class
+ 363
+ 364 Args:
+ 365 provider_name (str): Name of the IPTV provider
+ 366 provider_username (str): User name of the IPTV provider
+ 367 provider_password (str): Password of the IPTV provider
+ 368 provider_url (str): URL of the IPTV provider
+ 369 headers (dict): Requests Headers
+ 370 hide_adult_content(bool, optional): When `True` hide stream that are marked for adult
+ 371 cache_path (str, optional): Location where to save loaded files.
+ 372 Defaults to empty string.
+ 373 reload_time_sec (int, optional): Number of seconds before automatic reloading
+ 374 (-1 to turn it OFF)
+ 375 validate_json (bool, optional): Check Xtream API provided JSON for validity
+ 376 enable_flask (bool, optional): Enable Flask
+ 377 debug_flask (bool, optional): Enable the debug mode in Flask
+ 378
+ 379 Returns: XTream Class Instance
+ 380
+ 381 - Note 1: If it fails to authorize with provided username and password,
+ 382 auth_data will be an empty dictionary.
+ 383 - Note 2: The JSON validation option will take considerable amount of time and it should be
+ 384 used only as a debug tool. The Xtream API JSON from the provider passes through a
+ 385 schema that represent the best available understanding of how the Xtream API
+ 386 works.
+ 387 """
+ 388self.server=provider_url
+ 389self.username=provider_username
+ 390self.password=provider_password
+ 391self.name=provider_name
+ 392self.cache_path=cache_path
+ 393self.hide_adult_content=hide_adult_content
+ 394self.threshold_time_sec=reload_time_sec
+ 395self.validate_json=validate_json 396
- 397ifself.threshold_time_sec>0:
- 398print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
- 399else:
- 400print("Reload timer is OFF")
- 401
- 402ifself.state['authenticated']:
- 403ifUSE_FLASK:
- 404self.flaskapp=FlaskWrap('pyxtream',self,self.html_template_folder,debug=debug_flask)
- 405self.flaskapp.start()
- 406
- 407defsearch_stream(self,keyword:str,
- 408ignore_case:bool=True,
- 409return_type:str="LIST",
- 410stream_type:list=("series","movies","channels"))->list:
- 411"""Search for streams
- 412
- 413 Args:
- 414 keyword (str): Keyword to search for. Supports REGEX
- 415 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
- 416 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
- 417 stream_type (list, optional): Search within specific stream type.
- 418
- 419 Returns:
- 420 list: List with all the results, it could be empty.
- 421 """
- 422
- 423search_result=[]
- 424regex_flags=re.IGNORECASEifignore_caseelse0
- 425regex=re.compile(keyword,regex_flags)
- 426# if ignore_case:
- 427# regex = re.compile(keyword, re.IGNORECASE)
- 428# else:
- 429# regex = re.compile(keyword)
- 430
- 431# if "movies" in stream_type:
- 432# print(f"Checking {len(self.movies)} movies")
- 433# for stream in self.movies:
- 434# if re.match(regex, stream.name) is not None:
- 435# search_result.append(stream.export_json())
- 436
- 437# if "channels" in stream_type:
- 438# print(f"Checking {len(self.channels)} channels")
- 439# for stream in self.channels:
- 440# if re.match(regex, stream.name) is not None:
- 441# search_result.append(stream.export_json())
+ 397# get the pyxtream local path
+ 398self.app_fullpath=osp.dirname(osp.realpath(__file__))
+ 399
+ 400# prepare location of local html template
+ 401self.html_template_folder=osp.join(self.app_fullpath,"html")
+ 402
+ 403# if the cache_path is specified, test that it is a directory
+ 404ifself.cache_path!="":
+ 405# If the cache_path is not a directory, clear it
+ 406ifnotosp.isdir(self.cache_path):
+ 407print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
+ 408self.cache_path=""
+ 409
+ 410# If the cache_path is still empty, use default
+ 411ifself.cache_path=="":
+ 412self.cache_path=osp.expanduser("~/.xtream-cache/")
+ 413ifnotosp.isdir(self.cache_path):
+ 414makedirs(self.cache_path,exist_ok=True)
+ 415print(f"pyxtream cache path located at {self.cache_path}")
+ 416
+ 417ifheadersisnotNone:
+ 418self.connection_headers=headers
+ 419else:
+ 420self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
+ 421
+ 422self.authenticate()
+ 423
+ 424ifself.threshold_time_sec>0:
+ 425print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
+ 426else:
+ 427print("Reload timer is OFF")
+ 428
+ 429ifself.state['authenticated']:
+ 430ifUSE_FLASKandenable_flask:
+ 431print("Starting Web Interface")
+ 432self.flaskapp=FlaskWrap(
+ 433'pyxtream',self,self.html_template_folder,debug=debug_flask
+ 434)
+ 435self.flaskapp.start()
+ 436else:
+ 437print("Web interface not running")
+ 438
+ 439defget_download_progress(self,stream_id:int=None):
+ 440# TODO: Add check for stream specific ID
+ 441returnjson.dumps(self.download_progress) 442
- 443# if "series" in stream_type:
- 444# print(f"Checking {len(self.series)} series")
- 445# for stream in self.series:
- 446# if re.match(regex, stream.name) is not None:
- 447# search_result.append(stream.export_json())
- 448
- 449stream_collections={
- 450"movies":self.movies,
- 451"channels":self.channels,
- 452"series":self.series
- 453}
- 454
- 455forstream_type_nameinstream_type:
- 456ifstream_type_nameinstream_collections:
- 457collection=stream_collections[stream_type_name]
- 458print(f"Checking {len(collection)}{stream_type_name}")
- 459forstreamincollection:
- 460ifre.match(regex,stream.name)isnotNone:
- 461search_result.append(stream.export_json())
- 462else:
- 463print(f"`{stream_type_name}` not found in collection")
- 464
- 465ifreturn_type=="JSON":
- 466# if search_result is not None:
- 467print(f"Found {len(search_result)} results `{keyword}`")
- 468returnjson.dumps(search_result,ensure_ascii=False)
- 469
- 470returnsearch_result
- 471
- 472defdownload_video(self,stream_id:int)->str:
- 473"""Download Video from Stream ID
- 474
- 475 Args:
- 476 stream_id (int): Stirng identifing the stream ID
- 477
- 478 Returns:
- 479 str: Absolute Path Filename where the file was saved. Empty if could not download
- 480 """
- 481url=""
- 482filename=""
- 483forstreaminself.movies:
- 484ifstream.id==stream_id:
- 485url=stream.url
- 486fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
- 487filename=osp.join(self.cache_path,fn)
+ 443defget_last_7days(self):
+ 444returnjson.dumps(self.movies_7days,default=lambdax:x.export_json())
+ 445
+ 446defsearch_stream(self,keyword:str,
+ 447ignore_case:bool=True,
+ 448return_type:str="LIST",
+ 449stream_type:list=("series","movies","channels"),
+ 450added_after:datetime=None)->list:
+ 451"""Search for streams
+ 452
+ 453 Args:
+ 454 keyword (str): Keyword to search for. Supports REGEX
+ 455 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
+ 456 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
+ 457 stream_type (list, optional): Search within specific stream type.
+ 458 added_after (datetime, optional): Search for items that have been added after a certain date.
+ 459
+ 460 Returns:
+ 461 list: List with all the results, it could be empty.
+ 462 """
+ 463
+ 464search_result=[]
+ 465regex_flags=re.IGNORECASEifignore_caseelse0
+ 466regex=re.compile(keyword,regex_flags)
+ 467
+ 468stream_collections={
+ 469"movies":self.movies,
+ 470"channels":self.channels,
+ 471"series":self.series
+ 472}
+ 473
+ 474forstream_type_nameinstream_type:
+ 475ifstream_type_nameinstream_collections:
+ 476collection=stream_collections[stream_type_name]
+ 477print(f"Checking {len(collection)}{stream_type_name}")
+ 478forstreamincollection:
+ 479ifstream.nameandre.match(regex,stream.name)isnotNone:
+ 480ifadded_afterisNone:
+ 481# Add all matches
+ 482search_result.append(stream.export_json())
+ 483else:
+ 484# Only add if it is more recent
+ 485pass
+ 486else:
+ 487print(f"`{stream_type_name}` not found in collection") 488
- 489# If the url was correctly built and file does not exists, start downloading
- 490ifurl!="":
- 491#if not osp.isfile(filename):
- 492ifnotself._download_video_impl(url,filename):
- 493return"Error"
- 494
- 495returnfilename
- 496
- 497def_download_video_impl(self,url:str,fullpath_filename:str)->bool:
- 498"""Download a stream
- 499
- 500 Args:
- 501 url (str): Complete URL of the stream
- 502 fullpath_filename (str): Complete File path where to save the stream
- 503
- 504 Returns:
- 505 bool: True if successful, False if error
- 506 """
- 507ret_code=False
- 508mb_size=1024*1024
- 509try:
- 510print(f"Downloading from URL `{url}` and saving at `{fullpath_filename}`")
- 511
- 512# Check if the file already exists
- 513ifosp.exists(fullpath_filename):
- 514# If the file exists, resume the download from where it left off
- 515file_size=osp.getsize(fullpath_filename)
- 516self.connection_headers['Range']=f'bytes={file_size}-'
- 517mode='ab'# Append to the existing file
- 518print(f"Resuming from {file_size:_} bytes")
- 519else:
- 520# If the file does not exist, start a new download
- 521mode='wb'# Write a new file
- 522
- 523# Make the request to download
- 524response=requests.get(url,timeout=(10),stream=True,allow_redirects=True,headers=self.connection_headers)
- 525# If there is an answer from the remote server
- 526ifresponse.status_code==200orresponse.status_code==206:
- 527# Get content type Binary or Text
- 528content_type=response.headers.get('content-type',None)
- 529
- 530# Get total playlist byte size
- 531total_content_size=int(response.headers.get('content-length',None))
- 532total_content_size_mb=total_content_size/mb_size
- 533
- 534# Set downloaded size
- 535downloaded_bytes=0
- 536
- 537# Set stream blocks
- 538block_bytes=int(4*mb_size)# 4 MB
- 539
- 540print(f"Ready to download {total_content_size_mb:.1f} MB file ({total_content_size})")
- 541ifcontent_type.split('/')[0]!="text":
- 542withopen(fullpath_filename,mode)asfile:
- 543
- 544# Grab data by block_bytes
- 545fordatainresponse.iter_content(block_bytes,decode_unicode=False):
- 546downloaded_bytes+=block_bytes
- 547progress(downloaded_bytes,total_content_size,"Downloading")
- 548file.write(data)
- 549
- 550ifdownloaded_bytes==total_content_size:
- 551ret_code=True
- 552
- 553# Delete Range if it was added
- 554try:
- 555delself.connection_headers['Range']
- 556exceptKeyError:
- 557pass
- 558else:
- 559print(f"URL has a file with unexpected content-type {content_type}")
- 560else:
- 561print(f"HTTP error {response.status_code} while retrieving from {url}")
- 562exceptExceptionase:
- 563print(e)
- 564
- 565returnret_code
- 566
- 567def_slugify(self,string:str)->str:
- 568"""Normalize string
- 569
- 570 Normalizes string, converts to lowercase, removes non-alpha characters,
- 571 and converts spaces to hyphens.
+ 489ifreturn_type=="JSON":
+ 490# if search_result is not None:
+ 491print(f"Found {len(search_result)} results `{keyword}`")
+ 492returnjson.dumps(search_result,ensure_ascii=False)
+ 493
+ 494returnsearch_result
+ 495
+ 496defdownload_video(self,stream_id:int)->str:
+ 497"""Download Video from Stream ID
+ 498
+ 499 Args:
+ 500 stream_id (int): String identifying the stream ID
+ 501
+ 502 Returns:
+ 503 str: Absolute Path Filename where the file was saved. Empty if could not download
+ 504 """
+ 505url=""
+ 506filename=""
+ 507forseries_streaminself.series:
+ 508ifseries_stream.series_id==stream_id:
+ 509episode_object:Episode=series_stream.episodes["1"]
+ 510url=f"{series_stream.url}/{episode_object.id}."\
+ 511f"{episode_object.container_extension}"
+ 512
+ 513forstreaminself.movies:
+ 514ifstream.id==stream_id:
+ 515url=stream.url
+ 516fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
+ 517filename=osp.join(self.cache_path,fn)
+ 518
+ 519# If the url was correctly built and file does not exists, start downloading
+ 520ifurl!="":
+ 521ifnotself._download_video_impl(url,filename):
+ 522return"Error"
+ 523
+ 524returnfilename
+ 525
+ 526def_download_video_impl(self,url:str,fullpath_filename:str)->bool:
+ 527"""Download a stream
+ 528
+ 529 Args:
+ 530 url (str): Complete URL of the stream
+ 531 fullpath_filename (str): Complete File path where to save the stream
+ 532
+ 533 Returns:
+ 534 bool: True if successful, False if error
+ 535 """
+ 536ret_code=False
+ 537mb_size=1024*1024
+ 538try:
+ 539print(f"Downloading from URL `{url}` and saving at `{fullpath_filename}`")
+ 540
+ 541# Check if the file already exists
+ 542ifosp.exists(fullpath_filename):
+ 543# If the file exists, resume the download from where it left off
+ 544file_size=osp.getsize(fullpath_filename)
+ 545self.connection_headers['Range']=f'bytes={file_size}-'
+ 546mode='ab'# Append to the existing file
+ 547print(f"Resuming from {file_size:_} bytes")
+ 548else:
+ 549# If the file does not exist, start a new download
+ 550mode='wb'# Write a new file
+ 551
+ 552# Make the request to download
+ 553response=requests.get(
+ 554url,timeout=(10),
+ 555stream=True,
+ 556allow_redirects=True,
+ 557headers=self.connection_headers
+ 558)
+ 559# If there is an answer from the remote server
+ 560ifresponse.status_codein(200,206):
+ 561# Get content type Binary or Text
+ 562content_type=response.headers.get('content-type',None)
+ 563
+ 564# Get total playlist byte size
+ 565total_content_size=int(response.headers.get('content-length',None))
+ 566total_content_size_mb=total_content_size/mb_size
+ 567
+ 568# Set downloaded size
+ 569downloaded_bytes=0
+ 570self.download_progress['Total']=total_content_size
+ 571self.download_progress['Progress']=0 572
- 573 Args:
- 574 string (str): String to be normalized
+ 573# Set stream blocks
+ 574block_bytes=int(4*mb_size)# 4 MB 575
- 576 Returns:
- 577 str: Normalized String
- 578 """
- 579return"".join(x.lower()forxinstringifx.isprintable())
- 580
- 581def_validate_url(self,url:str)->bool:
- 582regex=re.compile(
- 583r"^(?:http|ftp)s?://"# http:// or https://
- 584r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"# domain...
- 585r"localhost|"# localhost...
- 586r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"# ...or ip
- 587r"(?::\d+)?"# optional port
- 588r"(?:/?|[/?]\S+)$",
- 589re.IGNORECASE,
- 590)
- 591
- 592returnre.match(regex,url)isnotNone
- 593
- 594def_get_logo_local_path(self,logo_url:str)->str:
- 595"""Convert the Logo URL to a local Logo Path
- 596
- 597 Args:
- 598 logoURL (str): The Logo URL
- 599
- 600 Returns:
- 601 [type]: The logo path as a string or None
- 602 """
- 603local_logo_path=None
- 604iflogo_urlisnotNone:
- 605ifnotself._validate_url(logo_url):
- 606logo_url=None
- 607else:
- 608local_logo_path=osp.join(
- 609self.cache_path,
- 610f"{self._slugify(self.name)}-{self._slugify(osp.split(logo_url)[-1])}"
- 611)
- 612returnlocal_logo_path
+ 576print(
+ 577f"Ready to download {total_content_size_mb:.1f} MB file ({total_content_size})"
+ 578)
+ 579ifcontent_type.split('/')[0]!="text":
+ 580withopen(fullpath_filename,mode)asfile:
+ 581
+ 582# Grab data by block_bytes
+ 583fordatainresponse.iter_content(block_bytes,decode_unicode=False):
+ 584downloaded_bytes+=block_bytes
+ 585progress(downloaded_bytes,total_content_size,"Downloading")
+ 586self.download_progress['Progress']=downloaded_bytes
+ 587file.write(data)
+ 588
+ 589ret_code=True
+ 590
+ 591# Delete Range if it was added
+ 592try:
+ 593delself.connection_headers['Range']
+ 594exceptKeyError:
+ 595pass
+ 596else:
+ 597print(f"URL has a file with unexpected content-type {content_type}")
+ 598else:
+ 599print(f"HTTP error {response.status_code} while retrieving from {url}")
+ 600exceptrequests.exceptions.ReadTimeout:
+ 601print("Read Timeout, try again")
+ 602exceptExceptionase:
+ 603print("Unknown error")
+ 604print(e)
+ 605
+ 606returnret_code
+ 607
+ 608def_slugify(self,string:str)->str:
+ 609"""Normalize string
+ 610
+ 611 Normalizes string, converts to lowercase, removes non-alpha characters,
+ 612 and converts spaces to hyphens. 613
- 614defauthenticate(self):
- 615"""Login to provider"""
- 616# If we have not yet successfully authenticated, attempt authentication
- 617ifself.state["authenticated"]isFalse:
- 618# Erase any previous data
- 619self.auth_data={}
- 620# Loop through 30 seconds
- 621i=0
- 622r=None
- 623# Prepare the authentication url
- 624url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 625print("Attempting connection... ",end='')
- 626whilei<30:
- 627try:
- 628# Request authentication, wait 4 seconds maximum
- 629r=requests.get(url,timeout=(4),headers=self.connection_headers)
- 630i=31
- 631exceptrequests.exceptions.ConnectionError:
- 632time.sleep(1)
- 633print(f"{i} ",end='',flush=True)
- 634i+=1
- 635
- 636ifrisnotNone:
- 637# If the answer is ok, process data and change state
- 638ifr.ok:
- 639print("Connected")
- 640self.auth_data=r.json()
- 641self.authorization={
- 642"username":self.auth_data["user_info"]["username"],
- 643"password":self.auth_data["user_info"]["password"]
- 644}
- 645# Account expiration date
- 646self.account_expiration=timedelta(
- 647seconds=(
- 648int(self.auth_data["user_info"]["exp_date"])-datetime.now().timestamp()
- 649)
- 650)
- 651# Mark connection authorized
- 652self.state["authenticated"]=True
- 653# Construct the base url for all requests
- 654self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 655# If there is a secure server connection, construct the base url SSL for all requests
- 656if"https_port"inself.auth_data["server_info"]:
- 657self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
- 658f"/player_api.php?username={self.username}&password={self.password}"
- 659print(f"Account expires in {str(self.account_expiration)}")
- 660else:
- 661print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
- 662else:
- 663print(f"\n{self.name}: Provider refused the connection")
- 664
- 665def_load_from_file(self,filename)->dict:
- 666"""Try to load the dictionary from file
- 667
- 668 Args:
- 669 filename ([type]): File name containing the data
- 670
- 671 Returns:
- 672 dict: Dictionary if found and no errors, None if file does not exists
- 673 """
- 674# Build the full path
- 675full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 614 Args:
+ 615 string (str): String to be normalized
+ 616
+ 617 Returns:
+ 618 str: Normalized String
+ 619 """
+ 620return"".join(x.lower()forxinstringifx.isprintable())
+ 621
+ 622def_validate_url(self,url:str)->bool:
+ 623regex=re.compile(
+ 624r"^(?:http|ftp)s?://"# http:// or https://
+ 625r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"# domain...
+ 626r"localhost|"# localhost...
+ 627r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"# ...or ip
+ 628r"(?::\d+)?"# optional port
+ 629r"(?:/?|[/?]\S+)$",
+ 630re.IGNORECASE,
+ 631)
+ 632
+ 633returnre.match(regex,url)isnotNone
+ 634
+ 635def_get_logo_local_path(self,logo_url:str)->str:
+ 636"""Convert the Logo URL to a local Logo Path
+ 637
+ 638 Args:
+ 639 logoURL (str): The Logo URL
+ 640
+ 641 Returns:
+ 642 [type]: The logo path as a string or None
+ 643 """
+ 644local_logo_path=None
+ 645iflogo_urlisnotNone:
+ 646ifnotself._validate_url(logo_url):
+ 647logo_url=None
+ 648else:
+ 649local_logo_path=osp.join(
+ 650self.cache_path,
+ 651f"{self._slugify(self.name)}-{self._slugify(osp.split(logo_url)[-1])}"
+ 652)
+ 653returnlocal_logo_path
+ 654
+ 655defauthenticate(self):
+ 656"""Login to provider"""
+ 657# If we have not yet successfully authenticated, attempt authentication
+ 658ifself.state["authenticated"]isFalse:
+ 659# Erase any previous data
+ 660self.auth_data={}
+ 661# Loop through 30 seconds
+ 662i=0
+ 663r=None
+ 664# Prepare the authentication url
+ 665url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 666print("Attempting connection... ",end='')
+ 667whilei<30:
+ 668try:
+ 669# Request authentication, wait 4 seconds maximum
+ 670r=requests.get(url,timeout=(4),headers=self.connection_headers)
+ 671i=31
+ 672except(requests.exceptions.ConnectionError,requests.exceptions.ReadTimeout):
+ 673time.sleep(1)
+ 674print(f"{i} ",end='',flush=True)
+ 675i+=1 676
- 677# If the cached file exists, attempt to load it
- 678ifosp.isfile(full_filename):
- 679
- 680my_data=None
- 681
- 682# Get the enlapsed seconds since last file update
- 683file_age_sec=time.time()-osp.getmtime(full_filename)
- 684# If the file was updated less than the threshold time,
- 685# it means that the file is still fresh, we can load it.
- 686# Otherwise skip and return None to force a re-download
- 687ifself.threshold_time_sec>file_age_sec:
- 688# Load the JSON data
- 689try:
- 690withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
- 691my_data=json.load(myfile)
- 692iflen(my_data)==0:
- 693my_data=None
- 694exceptExceptionase:
- 695print(f" - Could not load from file `{full_filename}`: e=`{e}`")
- 696returnmy_data
- 697
- 698returnNone
- 699
- 700def_save_to_file(self,data_list:dict,filename:str)->bool:
- 701"""Save a dictionary to file
- 702
- 703 This function will overwrite the file if already exists
- 704
- 705 Args:
- 706 data_list (dict): Dictionary to save
- 707 filename (str): Name of the file
+ 677ifrisnotNone:
+ 678# If the answer is ok, process data and change state
+ 679ifr.ok:
+ 680print("Connected")
+ 681self.auth_data=r.json()
+ 682self.authorization={
+ 683"username":self.auth_data["user_info"]["username"],
+ 684"password":self.auth_data["user_info"]["password"]
+ 685}
+ 686# Account expiration date
+ 687self.account_expiration=timedelta(
+ 688seconds=(
+ 689int(self.auth_data["user_info"]["exp_date"])-datetime.now().timestamp()
+ 690)
+ 691)
+ 692# Mark connection authorized
+ 693self.state["authenticated"]=True
+ 694# Construct the base url for all requests
+ 695self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 696# If there is a secure server connection, construct the base url SSL for all requests
+ 697if"https_port"inself.auth_data["server_info"]:
+ 698self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
+ 699f"/player_api.php?username={self.username}&password={self.password}"
+ 700print(f"Account expires in {str(self.account_expiration)}")
+ 701else:
+ 702print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
+ 703else:
+ 704print(f"\n{self.name}: Provider refused the connection")
+ 705
+ 706def_load_from_file(self,filename)->dict:
+ 707"""Try to load the dictionary from file 708
- 709 Returns:
- 710 bool: True if successfull, False if error
- 711 """
- 712ifdata_listisNone:
- 713returnFalse
- 714
- 715full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 716try:
- 717withopen(full_filename,mode="wt",encoding="utf-8")asfile:
- 718json.dump(data_list,file,ensure_ascii=False)
- 719returnTrue
- 720exceptExceptionase:
- 721print(f" - Could not save to file `{full_filename}`: e=`{e}`")
- 722returnFalse
- 723# if data_list is not None:
- 724
- 725# #Build the full path
- 726# full_filename = osp.join(self.cache_path, f"{self._slugify(self.name)}-{filename}")
- 727# # If the path makes sense, save the file
- 728# json_data = json.dumps(data_list, ensure_ascii=False)
- 729# try:
- 730# with open(full_filename, mode="wt", encoding="utf-8") as myfile:
- 731# myfile.write(json_data)
- 732# except Exception as e:
- 733# print(f" - Could not save to file `{full_filename}`: e=`{e}`")
- 734# return False
- 735
- 736# return True
- 737# else:
- 738# return False
- 739
- 740defload_iptv(self)->bool:
- 741"""Load XTream IPTV
- 742
- 743 - Add all Live TV to XTream.channels
- 744 - Add all VOD to XTream.movies
- 745 - Add all Series to XTream.series
- 746 Series contains Seasons and Episodes. Those are not automatically
- 747 retrieved from the server to reduce the loading time.
- 748 - Add all groups to XTream.groups
- 749 Groups are for all three channel types, Live TV, VOD, and Series
- 750
- 751 Returns:
- 752 bool: True if successfull, False if error
- 753 """
- 754# If pyxtream has not authenticated the connection, return empty
- 755ifself.state["authenticated"]isFalse:
- 756print("Warning, cannot load steams since authorization failed")
- 757returnFalse
- 758
- 759# If pyxtream has already loaded the data, skip and return success
- 760ifself.state["loaded"]isTrue:
- 761print("Warning, data has already been loaded.")
- 762returnTrue
- 763
- 764forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
- 765## Get GROUPS
- 766
- 767# Try loading local file
- 768dt=0
- 769start=timer()
- 770all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
- 771# If file empty or does not exists, download it from remote
- 772ifall_catisNone:
- 773# Load all Groups and save file locally
- 774all_cat=self._load_categories_from_provider(loading_stream_type)
- 775ifall_catisnotNone:
- 776self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
- 777dt=timer()-start
- 778
- 779# If we got the GROUPS data, show the statistics and load GROUPS
- 780ifall_catisnotNone:
- 781print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
- 782## Add GROUPS to dictionaries
+ 709 Args:
+ 710 filename ([type]): File name containing the data
+ 711
+ 712 Returns:
+ 713 dict: Dictionary if found and no errors, None if file does not exists
+ 714 """
+ 715# Build the full path
+ 716full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 717
+ 718# If the cached file exists, attempt to load it
+ 719ifosp.isfile(full_filename):
+ 720
+ 721my_data=None
+ 722
+ 723# Get the elapsed seconds since last file update
+ 724file_age_sec=time.time()-osp.getmtime(full_filename)
+ 725# If the file was updated less than the threshold time,
+ 726# it means that the file is still fresh, we can load it.
+ 727# Otherwise skip and return None to force a re-download
+ 728ifself.threshold_time_sec>file_age_sec:
+ 729# Load the JSON data
+ 730try:
+ 731withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
+ 732my_data=json.load(myfile)
+ 733iflen(my_data)==0:
+ 734my_data=None
+ 735exceptExceptionase:
+ 736print(f" - Could not load from file `{full_filename}`: e=`{e}`")
+ 737returnmy_data
+ 738
+ 739returnNone
+ 740
+ 741def_save_to_file(self,data_list:dict,filename:str)->bool:
+ 742"""Save a dictionary to file
+ 743
+ 744 This function will overwrite the file if already exists
+ 745
+ 746 Args:
+ 747 data_list (dict): Dictionary to save
+ 748 filename (str): Name of the file
+ 749
+ 750 Returns:
+ 751 bool: True if successful, False if error
+ 752 """
+ 753ifdata_listisNone:
+ 754returnFalse
+ 755
+ 756full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 757try:
+ 758withopen(full_filename,mode="wt",encoding="utf-8")asfile:
+ 759json.dump(data_list,file,ensure_ascii=False)
+ 760returnTrue
+ 761exceptExceptionase:
+ 762print(f" - Could not save to file `{full_filename}`: e=`{e}`")
+ 763returnFalse
+ 764
+ 765defload_iptv(self)->bool:
+ 766"""Load XTream IPTV
+ 767
+ 768 - Add all Live TV to XTream.channels
+ 769 - Add all VOD to XTream.movies
+ 770 - Add all Series to XTream.series
+ 771 Series contains Seasons and Episodes. Those are not automatically
+ 772 retrieved from the server to reduce the loading time.
+ 773 - Add all groups to XTream.groups
+ 774 Groups are for all three channel types, Live TV, VOD, and Series
+ 775
+ 776 Returns:
+ 777 bool: True if successful, False if error
+ 778 """
+ 779# If pyxtream has not authenticated the connection, return empty
+ 780ifself.state["authenticated"]isFalse:
+ 781print("Warning, cannot load steams since authorization failed")
+ 782returnFalse 783
- 784# Add the catch-all-errors group
- 785ifloading_stream_type==self.live_type:
- 786self.groups.append(self.live_catch_all_group)
- 787elifloading_stream_type==self.vod_type:
- 788self.groups.append(self.vod_catch_all_group)
- 789elifloading_stream_type==self.series_type:
- 790self.groups.append(self.series_catch_all_group)
- 791
- 792forcat_objinall_cat:
- 793ifschemaValidator(cat_obj,SchemaType.GROUP):
- 794# Create Group (Category)
- 795new_group=Group(cat_obj,loading_stream_type)
- 796# Add to xtream class
- 797self.groups.append(new_group)
- 798else:
- 799# Save what did not pass schema validation
- 800print(cat_obj)
- 801
- 802# Sort Categories
- 803self.groups.sort(key=lambdax:x.name)
- 804else:
- 805print(f" - Could not load {loading_stream_type} Groups")
- 806break
- 807
- 808## Get Streams
- 809
- 810# Try loading local file
- 811dt=0
- 812start=timer()
- 813all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
- 814# If file empty or does not exists, download it from remote
- 815ifall_streamsisNone:
- 816# Load all Streams and save file locally
- 817all_streams=self._load_streams_from_provider(loading_stream_type)
- 818self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
- 819dt=timer()-start
- 820
- 821# If we got the STREAMS data, show the statistics and load Streams
- 822ifall_streamsisnotNone:
- 823print(f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams in {dt:.3f} seconds")
- 824## Add Streams to dictionaries
+ 784# If pyxtream has already loaded the data, skip and return success
+ 785ifself.state["loaded"]isTrue:
+ 786print("Warning, data has already been loaded.")
+ 787returnTrue
+ 788
+ 789# Delete skipped channels from cache
+ 790full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 791try:
+ 792f=open(full_filename,mode="r+",encoding="utf-8")
+ 793f.truncate(0)
+ 794f.close()
+ 795exceptFileNotFoundError:
+ 796pass
+ 797
+ 798forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
+ 799# Get GROUPS
+ 800
+ 801# Try loading local file
+ 802dt=0
+ 803start=timer()
+ 804all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
+ 805# If file empty or does not exists, download it from remote
+ 806ifall_catisNone:
+ 807# Load all Groups and save file locally
+ 808all_cat=self._load_categories_from_provider(loading_stream_type)
+ 809ifall_catisnotNone:
+ 810self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
+ 811dt=timer()-start
+ 812
+ 813# If we got the GROUPS data, show the statistics and load GROUPS
+ 814ifall_catisnotNone:
+ 815print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
+ 816# Add GROUPS to dictionaries
+ 817
+ 818# Add the catch-all-errors group
+ 819ifloading_stream_type==self.live_type:
+ 820self.groups.append(self.live_catch_all_group)
+ 821elifloading_stream_type==self.vod_type:
+ 822self.groups.append(self.vod_catch_all_group)
+ 823elifloading_stream_type==self.series_type:
+ 824self.groups.append(self.series_catch_all_group) 825
- 826skipped_adult_content=0
- 827skipped_no_name_content=0
- 828
- 829number_of_streams=len(all_streams)
- 830current_stream_number=0
- 831# Calculate 1% of total number of streams
- 832# This is used to slow down the progress bar
- 833one_percent_number_of_streams=number_of_streams/100
- 834start=timer()
- 835forstream_channelinall_streams:
- 836skip_stream=False
- 837current_stream_number+=1
- 838
- 839# Show download progress every 1% of total number of streams
- 840ifcurrent_stream_number<one_percent_number_of_streams:
- 841progress(
- 842current_stream_number,
- 843number_of_streams,
- 844f"Processing {loading_stream_type} Streams"
- 845)
- 846one_percent_number_of_streams*=2
- 847
- 848# Validate JSON scheme
- 849ifself.validate_json:
- 850ifloading_stream_type==self.series_type:
- 851ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
- 852print(stream_channel)
- 853elifloading_stream_type==self.live_type:
- 854ifnotschemaValidator(stream_channel,SchemaType.LIVE):
- 855print(stream_channel)
- 856else:
- 857# vod_type
- 858ifnotschemaValidator(stream_channel,SchemaType.VOD):
- 859print(stream_channel)
- 860
- 861# Skip if the name of the stream is empty
- 862ifstream_channel["name"]=="":
- 863skip_stream=True
- 864skipped_no_name_content=skipped_no_name_content+1
- 865self._save_to_file_skipped_streams(stream_channel)
- 866
- 867# Skip if the user chose to hide adult streams
- 868ifself.hide_adult_contentandloading_stream_type==self.live_type:
- 869if"is_adult"instream_channel:
- 870ifstream_channel["is_adult"]=="1":
- 871skip_stream=True
- 872skipped_adult_content=skipped_adult_content+1
- 873self._save_to_file_skipped_streams(stream_channel)
- 874
- 875ifnotskip_stream:
- 876# Some channels have no group,
- 877# so let's add them to the catch all group
- 878ifstream_channel["category_id"]=="":
- 879stream_channel["category_id"]="9999"
- 880elifstream_channel["category_id"]!="1":
- 881pass
- 882
- 883# Find the first occurence of the group that the
- 884# Channel or Stream is pointing to
- 885the_group=next(
- 886(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
- 887None
- 888)
- 889
- 890# Set group title
- 891ifthe_groupisnotNone:
- 892group_title=the_group.name
- 893else:
- 894ifloading_stream_type==self.live_type:
- 895group_title=self.live_catch_all_group.name
- 896the_group=self.live_catch_all_group
- 897elifloading_stream_type==self.vod_type:
- 898group_title=self.vod_catch_all_group.name
- 899the_group=self.vod_catch_all_group
- 900elifloading_stream_type==self.series_type:
- 901group_title=self.series_catch_all_group.name
- 902the_group=self.series_catch_all_group
- 903
- 904
- 905ifloading_stream_type==self.series_type:
- 906# Load all Series
- 907new_series=Serie(self,stream_channel)
- 908# To get all the Episodes for every Season of each
- 909# Series is very time consuming, we will only
- 910# populate the Series once the user click on the
- 911# Series, the Seasons and Episodes will be loaded
- 912# using x.getSeriesInfoByID() function
- 913
- 914else:
- 915new_channel=Channel(
- 916self,
- 917group_title,
- 918stream_channel
- 919)
- 920
- 921ifnew_channel.group_id=="9999":
- 922print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+ 826forcat_objinall_cat:
+ 827ifschemaValidator(cat_obj,SchemaType.GROUP):
+ 828# Create Group (Category)
+ 829new_group=Group(cat_obj,loading_stream_type)
+ 830# Add to xtream class
+ 831self.groups.append(new_group)
+ 832else:
+ 833# Save what did not pass schema validation
+ 834print(cat_obj)
+ 835
+ 836# Sort Categories
+ 837self.groups.sort(key=lambdax:x.name)
+ 838else:
+ 839print(f" - Could not load {loading_stream_type} Groups")
+ 840break
+ 841
+ 842# Get Streams
+ 843
+ 844# Try loading local file
+ 845dt=0
+ 846start=timer()
+ 847all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
+ 848# If file empty or does not exists, download it from remote
+ 849ifall_streamsisNone:
+ 850# Load all Streams and save file locally
+ 851all_streams=self._load_streams_from_provider(loading_stream_type)
+ 852self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
+ 853dt=timer()-start
+ 854
+ 855# If we got the STREAMS data, show the statistics and load Streams
+ 856ifall_streamsisnotNone:
+ 857print(f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams in {dt:.3f} seconds")
+ 858# Add Streams to dictionaries
+ 859
+ 860skipped_adult_content=0
+ 861skipped_no_name_content=0
+ 862
+ 863number_of_streams=len(all_streams)
+ 864current_stream_number=0
+ 865# Calculate 1% of total number of streams
+ 866# This is used to slow down the progress bar
+ 867one_percent_number_of_streams=number_of_streams/100
+ 868start=timer()
+ 869forstream_channelinall_streams:
+ 870skip_stream=False
+ 871current_stream_number+=1
+ 872
+ 873# Show download progress every 1% of total number of streams
+ 874ifcurrent_stream_number<one_percent_number_of_streams:
+ 875progress(
+ 876current_stream_number,
+ 877number_of_streams,
+ 878f"Processing {loading_stream_type} Streams"
+ 879)
+ 880one_percent_number_of_streams*=2
+ 881
+ 882# Validate JSON scheme
+ 883ifself.validate_json:
+ 884ifloading_stream_type==self.series_type:
+ 885ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
+ 886print(stream_channel)
+ 887elifloading_stream_type==self.live_type:
+ 888ifnotschemaValidator(stream_channel,SchemaType.LIVE):
+ 889print(stream_channel)
+ 890else:
+ 891# vod_type
+ 892ifnotschemaValidator(stream_channel,SchemaType.VOD):
+ 893print(stream_channel)
+ 894
+ 895# Skip if the name of the stream is empty
+ 896ifstream_channel["name"]=="":
+ 897skip_stream=True
+ 898skipped_no_name_content=skipped_no_name_content+1
+ 899self._save_to_file_skipped_streams(stream_channel)
+ 900
+ 901# Skip if the user chose to hide adult streams
+ 902ifself.hide_adult_contentandloading_stream_type==self.live_type:
+ 903if"is_adult"instream_channel:
+ 904ifstream_channel["is_adult"]=="1":
+ 905skip_stream=True
+ 906skipped_adult_content=skipped_adult_content+1
+ 907self._save_to_file_skipped_streams(stream_channel)
+ 908
+ 909ifnotskip_stream:
+ 910# Some channels have no group,
+ 911# so let's add them to the catch all group
+ 912ifnotstream_channel["category_id"]:
+ 913stream_channel["category_id"]="9999"
+ 914elifstream_channel["category_id"]!="1":
+ 915pass
+ 916
+ 917# Find the first occurrence of the group that the
+ 918# Channel or Stream is pointing to
+ 919the_group=next(
+ 920(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
+ 921None
+ 922) 923
- 924# Save the new channel to the local list of channels
- 925ifloading_stream_type==self.live_type:
- 926self.channels.append(new_channel)
- 927elifloading_stream_type==self.vod_type:
- 928self.movies.append(new_channel)
- 929ifnew_channel.age_days_from_added<31:
- 930self.movies_30days.append(new_channel)
- 931ifnew_channel.age_days_from_added<7:
- 932self.movies_7days.append(new_channel)
- 933else:
- 934self.series.append(new_series)
- 935
- 936# Add stream to the specific Group
- 937ifthe_groupisnotNone:
- 938ifloading_stream_type!=self.series_type:
- 939the_group.channels.append(new_channel)
- 940else:
- 941the_group.series.append(new_series)
- 942else:
- 943print(f" - Group not found `{stream_channel['name']}`")
- 944print("\n")
- 945# Print information of which streams have been skipped
- 946ifself.hide_adult_content:
- 947print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
- 948ifskipped_no_name_content>0:
- 949print(f" - Skipped {skipped_no_name_content} "
- 950"unprintable {loading_stream_type} streams")
- 951else:
- 952print(f" - Could not load {loading_stream_type} Streams")
+ 924# Set group title
+ 925ifthe_groupisnotNone:
+ 926group_title=the_group.name
+ 927else:
+ 928ifloading_stream_type==self.live_type:
+ 929group_title=self.live_catch_all_group.name
+ 930the_group=self.live_catch_all_group
+ 931elifloading_stream_type==self.vod_type:
+ 932group_title=self.vod_catch_all_group.name
+ 933the_group=self.vod_catch_all_group
+ 934elifloading_stream_type==self.series_type:
+ 935group_title=self.series_catch_all_group.name
+ 936the_group=self.series_catch_all_group
+ 937
+ 938ifloading_stream_type==self.series_type:
+ 939# Load all Series
+ 940new_series=Serie(self,stream_channel)
+ 941# To get all the Episodes for every Season of each
+ 942# Series is very time consuming, we will only
+ 943# populate the Series once the user click on the
+ 944# Series, the Seasons and Episodes will be loaded
+ 945# using x.getSeriesInfoByID() function
+ 946
+ 947else:
+ 948new_channel=Channel(
+ 949self,
+ 950group_title,
+ 951stream_channel
+ 952) 953
- 954self.state["loaded"]=True
- 955
- 956def_save_to_file_skipped_streams(self,stream_channel:Channel):
- 957
- 958# Build the full path
- 959full_filename=osp.join(self.cache_path,"skipped_streams.json")
- 960
- 961# If the path makes sense, save the file
- 962json_data=json.dumps(stream_channel,ensure_ascii=False)
- 963try:
- 964withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
- 965myfile.writelines(json_data)
- 966returnTrue
- 967exceptExceptionase:
- 968print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
- 969returnFalse
- 970
- 971defget_series_info_by_id(self,get_series:dict):
- 972"""Get Seasons and Episodes for a Series
- 973
- 974 Args:
- 975 get_series (dict): Series dictionary
- 976 """
- 977
- 978series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
- 979
- 980ifseries_seasons["seasons"]isNone:
- 981series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
- 982
- 983forseries_infoinseries_seasons["seasons"]:
- 984season_name=series_info["name"]
- 985season_key=series_info['season_number']
- 986season=Season(season_name)
- 987get_series.seasons[season_name]=season
- 988if"episodes"inseries_seasons.keys():
- 989forseries_seasoninseries_seasons["episodes"].keys():
- 990forepisode_infoinseries_seasons["episodes"][str(series_season)]:
- 991new_episode_channel=Episode(
- 992self,series_info,"Testing",episode_info
- 993)
- 994season.episodes[episode_info["title"]]=new_episode_channel
- 995
- 996def_handle_request_exception(self,exception:requests.exceptions.RequestException):
- 997"""Handle different types of request exceptions."""
- 998ifisinstance(exception,requests.exceptions.ConnectionError):
- 999print(" - Connection Error: Possible network problem \
-1000 (e.g. DNS failure, refused connection, etc)")
-1001elifisinstance(exception,requests.exceptions.HTTPError):
-1002print(" - HTTP Error")
-1003elifisinstance(exception,requests.exceptions.TooManyRedirects):
-1004print(" - TooManyRedirects")
-1005elifisinstance(exception,requests.exceptions.ReadTimeout):
-1006print(" - Timeout while loading data")
-1007else:
-1008print(f" - An unexpected error occurred: {exception}")
-1009
-1010def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:
-1011"""Generic GET Request with Error handling
+ 954ifnew_channel.group_id=="9999":
+ 955print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+ 956
+ 957# Save the new channel to the local list of channels
+ 958ifloading_stream_type==self.live_type:
+ 959self.channels.append(new_channel)
+ 960elifloading_stream_type==self.vod_type:
+ 961self.movies.append(new_channel)
+ 962ifnew_channel.age_days_from_added<31:
+ 963self.movies_30days.append(new_channel)
+ 964ifnew_channel.age_days_from_added<7:
+ 965self.movies_7days.append(new_channel)
+ 966else:
+ 967self.series.append(new_series)
+ 968
+ 969# Add stream to the specific Group
+ 970ifthe_groupisnotNone:
+ 971ifloading_stream_type!=self.series_type:
+ 972the_group.channels.append(new_channel)
+ 973else:
+ 974the_group.series.append(new_series)
+ 975else:
+ 976print(f" - Group not found `{stream_channel['name']}`")
+ 977print("\n")
+ 978# Print information of which streams have been skipped
+ 979ifself.hide_adult_content:
+ 980print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
+ 981ifskipped_no_name_content>0:
+ 982print(f" - Skipped {skipped_no_name_content} "
+ 983"unprintable {loading_stream_type} streams")
+ 984else:
+ 985print(f" - Could not load {loading_stream_type} Streams")
+ 986
+ 987self.state["loaded"]=True
+ 988returnTrue
+ 989
+ 990def_save_to_file_skipped_streams(self,stream_channel:Channel):
+ 991
+ 992# Build the full path
+ 993full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 994
+ 995# If the path makes sense, save the file
+ 996json_data=json.dumps(stream_channel,ensure_ascii=False)
+ 997try:
+ 998withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
+ 999myfile.writelines(json_data)
+1000myfile.write('\n')
+1001returnTrue
+1002exceptExceptionase:
+1003print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
+1004returnFalse
+1005
+1006defget_series_info_by_id(self,get_series:dict):
+1007"""Get Seasons and Episodes for a Series
+1008
+1009 Args:
+1010 get_series (dict): Series dictionary
+1011 """1012
-1013 Args:
-1014 URL (str): The URL where to GET content
-1015 timeout (Tuple[int, int], optional): Connection and Downloading Timeout.
-1016 Defaults to (2,15).
-1017
-1018 Returns:
-1019 Optional[dict]: JSON dictionary of the loaded data, or None
-1020 """
-1021forattemptinrange(10):
-1022time.sleep(1)
-1023try:
-1024response=requests.get(url,timeout=timeout,headers=self.connection_headers)
-1025response.raise_for_status()# Raise an HTTPError for bad responses (4xx and 5xx)
-1026returnresponse.json()
-1027exceptrequests.exceptions.RequestExceptionase:
-1028self._handle_request_exception(e)
-1029
-1030returnNone
-1031# i = 0
-1032# while i < 10:
-1033# time.sleep(1)
-1034# try:
-1035# r = requests.get(url, timeout=timeout, headers=self.connection_headers)
-1036# i = 20
-1037# if r.status_code == 200:
-1038# return r.json()
-1039# except requests.exceptions.ConnectionError:
-1040# print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
-1041# i += 1
-1042
-1043# except requests.exceptions.HTTPError:
-1044# print(" - HTTP Error")
-1045# i += 1
-1046
-1047# except requests.exceptions.TooManyRedirects:
-1048# print(" - TooManyRedirects")
-1049# i += 1
-1050
-1051# except requests.exceptions.ReadTimeout:
-1052# print(" - Timeout while loading data")
-1053# i += 1
-1054
-1055# return None
-1056
-1057# GET Stream Categories
-1058def_load_categories_from_provider(self,stream_type:str):
-1059"""Get from provider all category for specific stream type from provider
-1060
-1061 Args:
-1062 stream_type (str): Stream type can be Live, VOD, Series
-1063
-1064 Returns:
-1065 [type]: JSON if successfull, otherwise None
-1066 """
-1067url=""
-1068ifstream_type==self.live_type:
-1069url=self.get_live_categories_URL()
-1070elifstream_type==self.vod_type:
-1071url=self.get_vod_cat_URL()
-1072elifstream_type==self.series_type:
-1073url=self.get_series_cat_URL()
-1074else:
-1075url=""
-1076
-1077returnself._get_request(url)
-1078
-1079# GET Streams
-1080def_load_streams_from_provider(self,stream_type:str):
-1081"""Get from provider all streams for specific stream type
+1013series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
+1014
+1015ifseries_seasons["seasons"]isNone:
+1016series_seasons["seasons"]=[
+1017{"name":"Season 1","cover":series_seasons["info"]["cover"]}
+1018]
+1019
+1020forseries_infoinseries_seasons["seasons"]:
+1021season_name=series_info["name"]
+1022season=Season(season_name)
+1023get_series.seasons[season_name]=season
+1024if"episodes"inseries_seasons.keys():
+1025forseries_seasoninseries_seasons["episodes"].keys():
+1026forepisode_infoinseries_seasons["episodes"][str(series_season)]:
+1027new_episode_channel=Episode(
+1028self,series_info,"Testing",episode_info
+1029)
+1030season.episodes[episode_info["title"]]=new_episode_channel
+1031
+1032def_handle_request_exception(self,exception:requests.exceptions.RequestException):
+1033"""Handle different types of request exceptions."""
+1034ifisinstance(exception,requests.exceptions.ConnectionError):
+1035print(" - Connection Error: Possible network problem \
+1036 (e.g. DNS failure, refused connection, etc)")
+1037elifisinstance(exception,requests.exceptions.HTTPError):
+1038print(" - HTTP Error")
+1039elifisinstance(exception,requests.exceptions.TooManyRedirects):
+1040print(" - TooManyRedirects")
+1041elifisinstance(exception,requests.exceptions.ReadTimeout):
+1042print(" - Timeout while loading data")
+1043else:
+1044print(f" - An unexpected error occurred: {exception}")
+1045
+1046def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:
+1047"""Generic GET Request with Error handling
+1048
+1049 Args:
+1050 URL (str): The URL where to GET content
+1051 timeout (Tuple[int, int], optional): Connection and Downloading Timeout.
+1052 Defaults to (2,15).
+1053
+1054 Returns:
+1055 Optional[dict]: JSON dictionary of the loaded data, or None
+1056 """
+1057
+1058kb_size=1024
+1059all_data=[]
+1060down_stats={"bytes":0,"kbytes":0,"mbytes":0,"start":0.0,"delta_sec":0.0}
+1061
+1062forattemptinrange(10):
+1063try:
+1064response=requests.get(
+1065url,
+1066stream=True,
+1067timeout=timeout,
+1068headers=self.connection_headers
+1069)
+1070response.raise_for_status()# Raise an HTTPError for bad responses (4xx and 5xx)
+1071break
+1072exceptrequests.exceptions.RequestExceptionase:
+1073self._handle_request_exception(e)
+1074returnNone
+1075
+1076# If there is an answer from the remote server
+1077ifresponse.status_codein(200,206):
+1078down_stats["start"]=time.perf_counter()
+1079
+1080# Set downloaded size
+1081down_stats["bytes"]=01082
-1083 Args:
-1084 stream_type (str): Stream type can be Live, VOD, Series
+1083# Set stream blocks
+1084block_bytes=int(1*kb_size*kb_size)# 4 MB1085
-1086 Returns:
-1087 [type]: JSON if successfull, otherwise None
-1088 """
-1089url=""
-1090ifstream_type==self.live_type:
-1091url=self.get_live_streams_URL()
-1092elifstream_type==self.vod_type:
-1093url=self.get_vod_streams_URL()
-1094elifstream_type==self.series_type:
-1095url=self.get_series_URL()
-1096else:
-1097url=""
-1098
-1099returnself._get_request(url)
-1100
-1101# GET Streams by Category
-1102def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
-1103"""Get from provider all streams for specific stream type with category/group ID
-1104
-1105 Args:
-1106 stream_type (str): Stream type can be Live, VOD, Series
-1107 category_id ([type]): Category/Group ID.
-1108
-1109 Returns:
-1110 [type]: JSON if successfull, otherwise None
-1111 """
-1112url=""
-1113
-1114ifstream_type==self.live_type:
-1115url=self.get_live_streams_URL_by_category(category_id)
-1116elifstream_type==self.vod_type:
-1117url=self.get_vod_streams_URL_by_category(category_id)
-1118elifstream_type==self.series_type:
-1119url=self.get_series_URL_by_category(category_id)
-1120else:
-1121url=""
-1122
-1123returnself._get_request(url)
-1124
-1125# GET SERIES Info
-1126def_load_series_info_by_id_from_provider(self,series_id:str):
-1127"""Gets informations about a Serie
-1128
-1129 Args:
-1130 series_id (str): Serie ID as described in Group
+1086# Grab data by block_bytes
+1087fordatainresponse.iter_content(block_bytes,decode_unicode=False):
+1088down_stats["bytes"]+=len(data)
+1089down_stats["kbytes"]=down_stats["bytes"]/kb_size
+1090down_stats["mbytes"]=down_stats["bytes"]/kb_size/kb_size
+1091down_stats["delta_sec"]=time.perf_counter()-down_stats["start"]
+1092download_speed_average=down_stats["kbytes"]//down_stats["delta_sec"]
+1093sys.stdout.write(
+1094f'\rDownloading {down_stats["kbytes"]:.1f} MB at {download_speed_average:.0f} kB/s'
+1095)
+1096sys.stdout.flush()
+1097all_data.append(data)
+1098print(" - Done")
+1099full_content=b''.join(all_data)
+1100returnjson.loads(full_content)
+1101
+1102print(f"HTTP error {response.status_code} while retrieving from {url}")
+1103
+1104returnNone
+1105
+1106# GET Stream Categories
+1107def_load_categories_from_provider(self,stream_type:str):
+1108"""Get from provider all category for specific stream type from provider
+1109
+1110 Args:
+1111 stream_type (str): Stream type can be Live, VOD, Series
+1112
+1113 Returns:
+1114 [type]: JSON if successful, otherwise None
+1115 """
+1116url=""
+1117ifstream_type==self.live_type:
+1118url=self.get_live_categories_URL()
+1119elifstream_type==self.vod_type:
+1120url=self.get_vod_cat_URL()
+1121elifstream_type==self.series_type:
+1122url=self.get_series_cat_URL()
+1123else:
+1124url=""
+1125
+1126returnself._get_request(url)
+1127
+1128# GET Streams
+1129def_load_streams_from_provider(self,stream_type:str):
+1130"""Get from provider all streams for specific stream type1131
-1132 Returns:
-1133 [type]: JSON if successfull, otherwise None
-1134 """
-1135returnself._get_request(self.get_series_info_URL_by_ID(series_id))
-1136
-1137# The seasons array, might be filled or might be completely empty.
-1138# If it is not empty, it will contain the cover, overview and the air date
-1139# of the selected season.
-1140# In your APP if you want to display the series, you have to take that
-1141# from the episodes array.
-1142
-1143# GET VOD Info
-1144defvodInfoByID(self,vod_id):
-1145returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
-1146
-1147# GET short_epg for LIVE Streams (same as stalker portal,
-1148# prints the next X EPG that will play soon)
-1149defliveEpgByStream(self,stream_id):
-1150returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
-1151
-1152defliveEpgByStreamAndLimit(self,stream_id,limit):
-1153returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
-1154
-1155# GET ALL EPG for LIVE Streams (same as stalker portal,
-1156# but it will print all epg listings regardless of the day)
-1157defallLiveEpgByStream(self,stream_id):
-1158returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
-1159
-1160# Full EPG List for all Streams
-1161defallEpg(self):
-1162returnself._get_request(self.get_all_epg_URL())
-1163
-1164## URL-builder methods
-1165defget_live_categories_URL(self)->str:
-1166returnf"{self.base_url}&action=get_live_categories"
-1167
-1168defget_live_streams_URL(self)->str:
-1169returnf"{self.base_url}&action=get_live_streams"
-1170
-1171defget_live_streams_URL_by_category(self,category_id)->str:
-1172returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
+1132 Args:
+1133 stream_type (str): Stream type can be Live, VOD, Series
+1134
+1135 Returns:
+1136 [type]: JSON if successful, otherwise None
+1137 """
+1138url=""
+1139ifstream_type==self.live_type:
+1140url=self.get_live_streams_URL()
+1141elifstream_type==self.vod_type:
+1142url=self.get_vod_streams_URL()
+1143elifstream_type==self.series_type:
+1144url=self.get_series_URL()
+1145else:
+1146url=""
+1147
+1148returnself._get_request(url)
+1149
+1150# GET Streams by Category
+1151def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
+1152"""Get from provider all streams for specific stream type with category/group ID
+1153
+1154 Args:
+1155 stream_type (str): Stream type can be Live, VOD, Series
+1156 category_id ([type]): Category/Group ID.
+1157
+1158 Returns:
+1159 [type]: JSON if successful, otherwise None
+1160 """
+1161url=""
+1162
+1163ifstream_type==self.live_type:
+1164url=self.get_live_streams_URL_by_category(category_id)
+1165elifstream_type==self.vod_type:
+1166url=self.get_vod_streams_URL_by_category(category_id)
+1167elifstream_type==self.series_type:
+1168url=self.get_series_URL_by_category(category_id)
+1169else:
+1170url=""
+1171
+1172returnself._get_request(url)1173
-1174defget_vod_cat_URL(self)->str:
-1175returnf"{self.base_url}&action=get_vod_categories"
-1176
-1177defget_vod_streams_URL(self)->str:
-1178returnf"{self.base_url}&action=get_vod_streams"
-1179
-1180defget_vod_streams_URL_by_category(self,category_id)->str:
-1181returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
-1182
-1183defget_series_cat_URL(self)->str:
-1184returnf"{self.base_url}&action=get_series_categories"
-1185
-1186defget_series_URL(self)->str:
-1187returnf"{self.base_url}&action=get_series"
-1188
-1189defget_series_URL_by_category(self,category_id)->str:
-1190returnf"{self.base_url}&action=get_series&category_id={category_id}"
-1191
-1192defget_series_info_URL_by_ID(self,series_id)->str:
-1193returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
-1194
-1195defget_VOD_info_URL_by_ID(self,vod_id)->str:
-1196returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
-1197
-1198defget_live_epg_URL_by_stream(self,stream_id)->str:
-1199returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
-1200
-1201defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
-1202returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
-1203
-1204defget_all_live_epg_URL_by_stream(self,stream_id)->str:
-1205returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
-1206
-1207defget_all_epg_URL(self)->str:
-1208returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
+1174# GET SERIES Info
+1175def_load_series_info_by_id_from_provider(self,series_id:str,return_type:str="DICT"):
+1176"""Gets information about a Serie
+1177
+1178 Args:
+1179 series_id (str): Serie ID as described in Group
+1180 return_type (str, optional): Output format, 'DICT' or 'JSON'. Defaults to "DICT".
+1181
+1182 Returns:
+1183 [type]: JSON if successful, otherwise None
+1184 """
+1185data=self._get_request(self.get_series_info_URL_by_ID(series_id))
+1186ifreturn_type=="JSON":
+1187returnjson.dumps(data,ensure_ascii=False)
+1188returndata
+1189
+1190# The seasons array, might be filled or might be completely empty.
+1191# If it is not empty, it will contain the cover, overview and the air date
+1192# of the selected season.
+1193# In your APP if you want to display the series, you have to take that
+1194# from the episodes array.
+1195
+1196# GET VOD Info
+1197defvodInfoByID(self,vod_id):
+1198returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
+1199
+1200# GET short_epg for LIVE Streams (same as stalker portal,
+1201# prints the next X EPG that will play soon)
+1202defliveEpgByStream(self,stream_id):
+1203returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
+1204
+1205defliveEpgByStreamAndLimit(self,stream_id,limit):
+1206returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
+1207
+1208# GET ALL EPG for LIVE Streams (same as stalker portal,
+1209# but it will print all epg listings regardless of the day)
+1210defallLiveEpgByStream(self,stream_id):
+1211returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
+1212
+1213# Full EPG List for all Streams
+1214defallEpg(self):
+1215returnself._get_request(self.get_all_epg_URL())
+1216
+1217# URL-builder methods
+1218defget_live_categories_URL(self)->str:
+1219returnf"{self.base_url}&action=get_live_categories"
+1220
+1221defget_live_streams_URL(self)->str:
+1222returnf"{self.base_url}&action=get_live_streams"
+1223
+1224defget_live_streams_URL_by_category(self,category_id)->str:
+1225returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
+1226
+1227defget_vod_cat_URL(self)->str:
+1228returnf"{self.base_url}&action=get_vod_categories"
+1229
+1230defget_vod_streams_URL(self)->str:
+1231returnf"{self.base_url}&action=get_vod_streams"
+1232
+1233defget_vod_streams_URL_by_category(self,category_id)->str:
+1234returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
+1235
+1236defget_series_cat_URL(self)->str:
+1237returnf"{self.base_url}&action=get_series_categories"
+1238
+1239defget_series_URL(self)->str:
+1240returnf"{self.base_url}&action=get_series"
+1241
+1242defget_series_URL_by_category(self,category_id)->str:
+1243returnf"{self.base_url}&action=get_series&category_id={category_id}"
+1244
+1245defget_series_info_URL_by_ID(self,series_id)->str:
+1246returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
+1247
+1248defget_VOD_info_URL_by_ID(self,vod_id)->str:
+1249returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
+1250
+1251defget_live_epg_URL_by_stream(self,stream_id)->str:
+1252returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
+1253
+1254defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
+1255returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
+1256
+1257defget_all_live_epg_URL_by_stream(self,stream_id)->str:
+1258returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
+1259
+1260defget_all_epg_URL(self)->str:
+1261returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
@@ -1660,92 +1734,96 @@
-
43classChannel:
- 44# Required by Hypnotix
- 45info=""
- 46id=""
- 47name=""# What is the difference between the below name and title?
- 48logo=""
- 49logo_path=""
- 50group_title=""
- 51title=""
- 52url=""
- 53
- 54# XTream
- 55stream_type:str=""
- 56group_id:str=""
- 57is_adult:int=0
- 58added:int=0
- 59epg_channel_id:str=""
- 60age_days_from_added:int=0
- 61date_now:datetime
- 62
- 63# This contains the raw JSON data
- 64raw=""
- 65
- 66def__init__(self,xtream:object,group_title,stream_info):
- 67self.date_now=datetime.now()
- 68
- 69stream_type=stream_info["stream_type"]
- 70# Adjust the odd "created_live" type
- 71ifstream_typein("created_live","radio_streams"):
- 72stream_type="live"
- 73
- 74ifstream_typenotin("live","movie"):
- 75print(f"Error the channel has unknown stream type `{stream_type}`\n`{stream_info}`")
- 76else:
- 77# Raw JSON Channel
- 78self.raw=stream_info
- 79
- 80stream_name=stream_info["name"]
- 81
- 82# Required by Hypnotix
- 83self.id=stream_info["stream_id"]
- 84self.name=stream_name
- 85self.logo=stream_info["stream_icon"]
- 86self.logo_path=xtream._get_logo_local_path(self.logo)
- 87self.group_title=group_title
- 88self.title=stream_name
- 89
- 90# Check if category_id key is available
- 91if"category_id"instream_info.keys():
- 92self.group_id=int(stream_info["category_id"])
- 93
- 94ifstream_type=="live":
- 95stream_extension="ts"
+
44classChannel:
+ 45# Required by Hypnotix
+ 46info=""
+ 47id=""
+ 48name=""# What is the difference between the below name and title?
+ 49logo=""
+ 50logo_path=""
+ 51group_title=""
+ 52title=""
+ 53url=""
+ 54
+ 55# XTream
+ 56stream_type:str=""
+ 57group_id:str=""
+ 58is_adult:int=0
+ 59added:int=0
+ 60epg_channel_id:str=""
+ 61age_days_from_added:int=0
+ 62date_now:datetime
+ 63
+ 64# This contains the raw JSON data
+ 65raw=""
+ 66
+ 67def__init__(self,xtream:object,group_title,stream_info):
+ 68self.date_now=datetime.now()
+ 69
+ 70stream_type=stream_info["stream_type"]
+ 71# Adjust the odd "created_live" type
+ 72ifstream_typein("created_live","radio_streams"):
+ 73stream_type="live"
+ 74
+ 75ifstream_typenotin("live","movie"):
+ 76print(f"Error the channel has unknown stream type `{stream_type}`\n`{stream_info}`")
+ 77else:
+ 78# Raw JSON Channel
+ 79self.raw=stream_info
+ 80
+ 81stream_name=stream_info["name"]
+ 82
+ 83# Required by Hypnotix
+ 84self.id=stream_info["stream_id"]
+ 85self.name=stream_name
+ 86self.logo=stream_info["stream_icon"]
+ 87self.logo_path=xtream._get_logo_local_path(self.logo)
+ 88self.group_title=group_title
+ 89self.title=stream_name
+ 90
+ 91# Check if category_id key is available
+ 92if"category_id"instream_info.keys():
+ 93self.group_id=int(stream_info["category_id"])
+ 94
+ 95stream_extension="" 96
- 97# Check if epg_channel_id key is available
- 98if"epg_channel_id"instream_info.keys():
- 99self.epg_channel_id=stream_info["epg_channel_id"]
-100
-101elifstream_type=="movie":
-102stream_extension=stream_info["container_extension"]
+ 97ifstream_type=="live":
+ 98stream_extension="ts"
+ 99
+100# Check if epg_channel_id key is available
+101if"epg_channel_id"instream_info.keys():
+102self.epg_channel_id=stream_info["epg_channel_id"]103
-104# Default to 0
-105self.is_adult=0
-106# Check if is_adult key is available
-107if"is_adult"instream_info.keys():
-108self.is_adult=int(stream_info["is_adult"])
-109
-110self.added=int(stream_info["added"])
-111self.age_days_from_added=abs(datetime.utcfromtimestamp(self.added)-self.date_now).days
+104elifstream_type=="movie":
+105stream_extension=stream_info["container_extension"]
+106
+107# Default to 0
+108self.is_adult=0
+109# Check if is_adult key is available
+110if"is_adult"instream_info.keys():
+111self.is_adult=int(stream_info["is_adult"])112
-113# Required by Hypnotix
-114self.url=f"{xtream.server}/{stream_type}/{xtream.authorization['username']}/" \
-115f"{xtream.authorization['password']}/{stream_info['stream_id']}.{stream_extension}"
-116
-117# Check that the constructed URL is valid
-118ifnotxtream._validate_url(self.url):
-119print(f"{self.name} - Bad URL? `{self.url}`")
-120
-121defexport_json(self):
-122jsondata={}
-123
-124jsondata["url"]=self.url
-125jsondata.update(self.raw)
-126jsondata["logo_path"]=self.logo_path
-127
-128returnjsondata
+113self.added=int(stream_info["added"])
+114self.age_days_from_added=abs(
+115datetime.utcfromtimestamp(self.added)-self.date_now
+116).days
+117
+118# Required by Hypnotix
+119self.url=f"{xtream.server}/{stream_type}/{xtream.authorization['username']}/" \
+120f"{xtream.authorization['password']}/{stream_info['stream_id']}.{stream_extension}"
+121
+122# Check that the constructed URL is valid
+123ifnotxtream._validate_url(self.url):
+124print(f"{self.name} - Bad URL? `{self.url}`")
+125
+126defexport_json(self):
+127jsondata={}
+128
+129jsondata["url"]=self.url
+130jsondata.update(self.raw)
+131jsondata["logo_path"]=self.logo_path
+132
+133returnjsondata
@@ -1761,60 +1839,64 @@
-
66def__init__(self,xtream:object,group_title,stream_info):
- 67self.date_now=datetime.now()
- 68
- 69stream_type=stream_info["stream_type"]
- 70# Adjust the odd "created_live" type
- 71ifstream_typein("created_live","radio_streams"):
- 72stream_type="live"
- 73
- 74ifstream_typenotin("live","movie"):
- 75print(f"Error the channel has unknown stream type `{stream_type}`\n`{stream_info}`")
- 76else:
- 77# Raw JSON Channel
- 78self.raw=stream_info
- 79
- 80stream_name=stream_info["name"]
- 81
- 82# Required by Hypnotix
- 83self.id=stream_info["stream_id"]
- 84self.name=stream_name
- 85self.logo=stream_info["stream_icon"]
- 86self.logo_path=xtream._get_logo_local_path(self.logo)
- 87self.group_title=group_title
- 88self.title=stream_name
- 89
- 90# Check if category_id key is available
- 91if"category_id"instream_info.keys():
- 92self.group_id=int(stream_info["category_id"])
- 93
- 94ifstream_type=="live":
- 95stream_extension="ts"
+
67def__init__(self,xtream:object,group_title,stream_info):
+ 68self.date_now=datetime.now()
+ 69
+ 70stream_type=stream_info["stream_type"]
+ 71# Adjust the odd "created_live" type
+ 72ifstream_typein("created_live","radio_streams"):
+ 73stream_type="live"
+ 74
+ 75ifstream_typenotin("live","movie"):
+ 76print(f"Error the channel has unknown stream type `{stream_type}`\n`{stream_info}`")
+ 77else:
+ 78# Raw JSON Channel
+ 79self.raw=stream_info
+ 80
+ 81stream_name=stream_info["name"]
+ 82
+ 83# Required by Hypnotix
+ 84self.id=stream_info["stream_id"]
+ 85self.name=stream_name
+ 86self.logo=stream_info["stream_icon"]
+ 87self.logo_path=xtream._get_logo_local_path(self.logo)
+ 88self.group_title=group_title
+ 89self.title=stream_name
+ 90
+ 91# Check if category_id key is available
+ 92if"category_id"instream_info.keys():
+ 93self.group_id=int(stream_info["category_id"])
+ 94
+ 95stream_extension="" 96
- 97# Check if epg_channel_id key is available
- 98if"epg_channel_id"instream_info.keys():
- 99self.epg_channel_id=stream_info["epg_channel_id"]
-100
-101elifstream_type=="movie":
-102stream_extension=stream_info["container_extension"]
+ 97ifstream_type=="live":
+ 98stream_extension="ts"
+ 99
+100# Check if epg_channel_id key is available
+101if"epg_channel_id"instream_info.keys():
+102self.epg_channel_id=stream_info["epg_channel_id"]103
-104# Default to 0
-105self.is_adult=0
-106# Check if is_adult key is available
-107if"is_adult"instream_info.keys():
-108self.is_adult=int(stream_info["is_adult"])
-109
-110self.added=int(stream_info["added"])
-111self.age_days_from_added=abs(datetime.utcfromtimestamp(self.added)-self.date_now).days
+104elifstream_type=="movie":
+105stream_extension=stream_info["container_extension"]
+106
+107# Default to 0
+108self.is_adult=0
+109# Check if is_adult key is available
+110if"is_adult"instream_info.keys():
+111self.is_adult=int(stream_info["is_adult"])112
-113# Required by Hypnotix
-114self.url=f"{xtream.server}/{stream_type}/{xtream.authorization['username']}/" \
-115f"{xtream.authorization['password']}/{stream_info['stream_id']}.{stream_extension}"
-116
-117# Check that the constructed URL is valid
-118ifnotxtream._validate_url(self.url):
-119print(f"{self.name} - Bad URL? `{self.url}`")
+113self.added=int(stream_info["added"])
+114self.age_days_from_added=abs(
+115datetime.utcfromtimestamp(self.added)-self.date_now
+116).days
+117
+118# Required by Hypnotix
+119self.url=f"{xtream.server}/{stream_type}/{xtream.authorization['username']}/" \
+120f"{xtream.authorization['password']}/{stream_info['stream_id']}.{stream_extension}"
+121
+122# Check that the constructed URL is valid
+123ifnotxtream._validate_url(self.url):
+124print(f"{self.name} - Bad URL? `{self.url}`")
131classGroup:
-132# Required by Hypnotix
-133name=""
-134group_type=""
-135
-136# XTream
-137group_id=""
-138
-139# This contains the raw JSON data
-140raw=""
-141
-142defconvert_region_shortname_to_fullname(self,shortname):
+
136classGroup:
+137# Required by Hypnotix
+138name=""
+139group_type=""
+140
+141# XTream
+142group_id=""143
-144ifshortname=="AR":
-145return"Arab"
-146ifshortname=="AM":
-147return"America"
-148ifshortname=="AS":
-149return"Asia"
-150ifshortname=="AF":
-151return"Africa"
-152ifshortname=="EU":
-153return"Europe"
-154
-155return""
-156
-157def__init__(self,group_info:dict,stream_type:str):
-158# Raw JSON Group
-159self.raw=group_info
-160
-161self.channels=[]
-162self.series=[]
-163
-164TV_GROUP,MOVIES_GROUP,SERIES_GROUP=range(3)
+144# This contains the raw JSON data
+145raw=""
+146
+147defconvert_region_shortname_to_fullname(self,shortname):
+148
+149ifshortname=="AR":
+150return"Arab"
+151ifshortname=="AM":
+152return"America"
+153ifshortname=="AS":
+154return"Asia"
+155ifshortname=="AF":
+156return"Africa"
+157ifshortname=="EU":
+158return"Europe"
+159
+160return""
+161
+162def__init__(self,group_info:dict,stream_type:str):
+163# Raw JSON Group
+164self.raw=group_info165
-166if"VOD"==stream_type:
-167self.group_type=MOVIES_GROUP
-168elif"Series"==stream_type:
-169self.group_type=SERIES_GROUP
-170elif"Live"==stream_type:
-171self.group_type=TV_GROUP
-172else:
-173print(f"Unrecognized stream type `{stream_type}` for `{group_info}`")
-174
-175self.name=group_info["category_name"]
-176split_name=self.name.split('|')
-177self.region_shortname=""
-178self.region_longname=""
-179iflen(split_name)>1:
-180self.region_shortname=split_name[0].strip()
-181self.region_longname=self.convert_region_shortname_to_fullname(self.region_shortname)
-182
-183# Check if category_id key is available
-184if"category_id"ingroup_info.keys():
-185self.group_id=int(group_info["category_id"])
+166self.channels=[]
+167self.series=[]
+168
+169TV_GROUP,MOVIES_GROUP,SERIES_GROUP=range(3)
+170
+171if"VOD"==stream_type:
+172self.group_type=MOVIES_GROUP
+173elif"Series"==stream_type:
+174self.group_type=SERIES_GROUP
+175elif"Live"==stream_type:
+176self.group_type=TV_GROUP
+177else:
+178print(f"Unrecognized stream type `{stream_type}` for `{group_info}`")
+179
+180self.name=group_info["category_name"]
+181split_name=self.name.split('|')
+182self.region_shortname=""
+183self.region_longname=""
+184iflen(split_name)>1:
+185self.region_shortname=split_name[0].strip()
+186self.region_longname=self.convert_region_shortname_to_fullname(self.region_shortname)
+187
+188# Check if category_id key is available
+189if"category_id"ingroup_info.keys():
+190self.group_id=int(group_info["category_id"])
@@ -2119,35 +2201,35 @@
-
157def__init__(self,group_info:dict,stream_type:str):
-158# Raw JSON Group
-159self.raw=group_info
-160
-161self.channels=[]
-162self.series=[]
-163
-164TV_GROUP,MOVIES_GROUP,SERIES_GROUP=range(3)
+
162def__init__(self,group_info:dict,stream_type:str):
+163# Raw JSON Group
+164self.raw=group_info165
-166if"VOD"==stream_type:
-167self.group_type=MOVIES_GROUP
-168elif"Series"==stream_type:
-169self.group_type=SERIES_GROUP
-170elif"Live"==stream_type:
-171self.group_type=TV_GROUP
-172else:
-173print(f"Unrecognized stream type `{stream_type}` for `{group_info}`")
-174
-175self.name=group_info["category_name"]
-176split_name=self.name.split('|')
-177self.region_shortname=""
-178self.region_longname=""
-179iflen(split_name)>1:
-180self.region_shortname=split_name[0].strip()
-181self.region_longname=self.convert_region_shortname_to_fullname(self.region_shortname)
-182
-183# Check if category_id key is available
-184if"category_id"ingroup_info.keys():
-185self.group_id=int(group_info["category_id"])
+166self.channels=[]
+167self.series=[]
+168
+169TV_GROUP,MOVIES_GROUP,SERIES_GROUP=range(3)
+170
+171if"VOD"==stream_type:
+172self.group_type=MOVIES_GROUP
+173elif"Series"==stream_type:
+174self.group_type=SERIES_GROUP
+175elif"Live"==stream_type:
+176self.group_type=TV_GROUP
+177else:
+178print(f"Unrecognized stream type `{stream_type}` for `{group_info}`")
+179
+180self.name=group_info["category_name"]
+181split_name=self.name.split('|')
+182self.region_shortname=""
+183self.region_longname=""
+184iflen(split_name)>1:
+185self.region_shortname=split_name[0].strip()
+186self.region_longname=self.convert_region_shortname_to_fullname(self.region_shortname)
+187
+188# Check if category_id key is available
+189if"category_id"ingroup_info.keys():
+190self.group_id=int(group_info["category_id"])
188classEpisode:
-189# Required by Hypnotix
-190title=""
-191name=""
-192info=""
-193
-194# XTream
-195
-196# This contains the raw JSON data
-197raw=""
+
193classEpisode:
+194# Required by Hypnotix
+195title=""
+196name=""
+197info=""198
-199def__init__(self,xtream:object,series_info,group_title,episode_info)->None:
-200# Raw JSON Episode
-201self.raw=episode_info
-202
-203self.title=episode_info["title"]
-204self.name=self.title
-205self.group_title=group_title
-206self.id=episode_info["id"]
-207self.container_extension=episode_info["container_extension"]
-208self.episode_number=episode_info["episode_num"]
-209self.av_info=episode_info["info"]
-210
-211self.logo=series_info["cover"]
-212self.logo_path=xtream._get_logo_local_path(self.logo)
-213
-214self.url=f"{xtream.server}/series/" \
-215f"{xtream.authorization['username']}/" \
-216f"{xtream.authorization['password']}/{self.id}.{self.container_extension}"
-217
-218# Check that the constructed URL is valid
-219ifnotxtream._validate_url(self.url):
-220print(f"{self.name} - Bad URL? `{self.url}`")
+199# XTream
+200
+201# This contains the raw JSON data
+202raw=""
+203
+204def__init__(self,xtream:object,series_info,group_title,episode_info)->None:
+205# Raw JSON Episode
+206self.raw=episode_info
+207
+208self.title=episode_info["title"]
+209self.name=self.title
+210self.group_title=group_title
+211self.id=episode_info["id"]
+212self.container_extension=episode_info["container_extension"]
+213self.episode_number=episode_info["episode_num"]
+214self.av_info=episode_info["info"]
+215
+216self.logo=series_info["cover"]
+217self.logo_path=xtream._get_logo_local_path(self.logo)
+218
+219self.url=f"{xtream.server}/series/" \
+220f"{xtream.authorization['username']}/" \
+221f"{xtream.authorization['password']}/{self.id}.{self.container_extension}"
+222
+223# Check that the constructed URL is valid
+224ifnotxtream._validate_url(self.url):
+225print(f"{self.name} - Bad URL? `{self.url}`")
@@ -2337,28 +2419,28 @@
-
199def__init__(self,xtream:object,series_info,group_title,episode_info)->None:
-200# Raw JSON Episode
-201self.raw=episode_info
-202
-203self.title=episode_info["title"]
-204self.name=self.title
-205self.group_title=group_title
-206self.id=episode_info["id"]
-207self.container_extension=episode_info["container_extension"]
-208self.episode_number=episode_info["episode_num"]
-209self.av_info=episode_info["info"]
-210
-211self.logo=series_info["cover"]
-212self.logo_path=xtream._get_logo_local_path(self.logo)
-213
-214self.url=f"{xtream.server}/series/" \
-215f"{xtream.authorization['username']}/" \
-216f"{xtream.authorization['password']}/{self.id}.{self.container_extension}"
-217
-218# Check that the constructed URL is valid
-219ifnotxtream._validate_url(self.url):
-220print(f"{self.name} - Bad URL? `{self.url}`")
+
204def__init__(self,xtream:object,series_info,group_title,episode_info)->None:
+205# Raw JSON Episode
+206self.raw=episode_info
+207
+208self.title=episode_info["title"]
+209self.name=self.title
+210self.group_title=group_title
+211self.id=episode_info["id"]
+212self.container_extension=episode_info["container_extension"]
+213self.episode_number=episode_info["episode_num"]
+214self.av_info=episode_info["info"]
+215
+216self.logo=series_info["cover"]
+217self.logo_path=xtream._get_logo_local_path(self.logo)
+218
+219self.url=f"{xtream.server}/series/" \
+220f"{xtream.authorization['username']}/" \
+221f"{xtream.authorization['password']}/{self.id}.{self.container_extension}"
+222
+223# Check that the constructed URL is valid
+224ifnotxtream._validate_url(self.url):
+225print(f"{self.name} - Bad URL? `{self.url}`")
@@ -2513,57 +2595,64 @@
-
223classSerie:
-224# Required by Hypnotix
-225name=""
-226logo=""
-227logo_path=""
-228
-229# XTream
-230series_id=""
-231plot=""
-232youtube_trailer=""
-233genre=""
-234
-235# This contains the raw JSON data
-236raw=""
-237
-238def__init__(self,xtream:object,series_info):
-239# Raw JSON Series
-240self.raw=series_info
-241self.xtream=xtream
+
228classSerie:
+229# Required by Hypnotix
+230name=""
+231logo=""
+232logo_path=""
+233
+234# XTream
+235series_id=""
+236plot=""
+237youtube_trailer=""
+238genre=""
+239
+240# This contains the raw JSON data
+241raw=""242
-243# Required by Hypnotix
-244self.name=series_info["name"]
-245self.logo=series_info["cover"]
-246self.logo_path=xtream._get_logo_local_path(self.logo)
-247
-248self.seasons={}
-249self.episodes={}
+243def__init__(self,xtream:object,series_info):
+244
+245series_info["added"]=series_info["last_modified"]
+246
+247# Raw JSON Series
+248self.raw=series_info
+249self.xtream=xtream250
-251# Check if category_id key is available
-252if"series_id"inseries_info.keys():
-253self.series_id=int(series_info["series_id"])
-254
-255# Check if plot key is available
-256if"plot"inseries_info.keys():
-257self.plot=series_info["plot"]
+251# Required by Hypnotix
+252self.name=series_info["name"]
+253self.logo=series_info["cover"]
+254self.logo_path=xtream._get_logo_local_path(self.logo)
+255
+256self.seasons={}
+257self.episodes={}258
-259# Check if youtube_trailer key is available
-260if"youtube_trailer"inseries_info.keys():
-261self.youtube_trailer=series_info["youtube_trailer"]
+259# Check if category_id key is available
+260if"series_id"inseries_info.keys():
+261self.series_id=int(series_info["series_id"])262
-263# Check if genre key is available
-264if"genre"inseries_info.keys():
-265self.genre=series_info["genre"]
+263# Check if plot key is available
+264if"plot"inseries_info.keys():
+265self.plot=series_info["plot"]266
-267defexport_json(self):
-268jsondata={}
-269
-270jsondata.update(self.raw)
-271jsondata['logo_path']=self.logo_path
-272
-273returnjsondata
+267# Check if youtube_trailer key is available
+268if"youtube_trailer"inseries_info.keys():
+269self.youtube_trailer=series_info["youtube_trailer"]
+270
+271# Check if genre key is available
+272if"genre"inseries_info.keys():
+273self.genre=series_info["genre"]
+274
+275self.url=f"{xtream.server}/series/" \
+276f"{xtream.authorization['username']}/" \
+277f"{xtream.authorization['password']}/{self.series_id}/"
+278
+279defexport_json(self):
+280jsondata={}
+281
+282jsondata.update(self.raw)
+283jsondata['logo_path']=self.logo_path
+284
+285returnjsondata
@@ -2579,34 +2668,41 @@
-
238def__init__(self,xtream:object,series_info):
-239# Raw JSON Series
-240self.raw=series_info
-241self.xtream=xtream
-242
-243# Required by Hypnotix
-244self.name=series_info["name"]
-245self.logo=series_info["cover"]
-246self.logo_path=xtream._get_logo_local_path(self.logo)
-247
-248self.seasons={}
-249self.episodes={}
+
243def__init__(self,xtream:object,series_info):
+244
+245series_info["added"]=series_info["last_modified"]
+246
+247# Raw JSON Series
+248self.raw=series_info
+249self.xtream=xtream250
-251# Check if category_id key is available
-252if"series_id"inseries_info.keys():
-253self.series_id=int(series_info["series_id"])
-254
-255# Check if plot key is available
-256if"plot"inseries_info.keys():
-257self.plot=series_info["plot"]
+251# Required by Hypnotix
+252self.name=series_info["name"]
+253self.logo=series_info["cover"]
+254self.logo_path=xtream._get_logo_local_path(self.logo)
+255
+256self.seasons={}
+257self.episodes={}258
-259# Check if youtube_trailer key is available
-260if"youtube_trailer"inseries_info.keys():
-261self.youtube_trailer=series_info["youtube_trailer"]
+259# Check if category_id key is available
+260if"series_id"inseries_info.keys():
+261self.series_id=int(series_info["series_id"])262
-263# Check if genre key is available
-264if"genre"inseries_info.keys():
-265self.genre=series_info["genre"]
+263# Check if plot key is available
+264if"plot"inseries_info.keys():
+265self.plot=series_info["plot"]
+266
+267# Check if youtube_trailer key is available
+268if"youtube_trailer"inseries_info.keys():
+269self.youtube_trailer=series_info["youtube_trailer"]
+270
+271# Check if genre key is available
+272if"genre"inseries_info.keys():
+273self.genre=series_info["genre"]
+274
+275self.url=f"{xtream.server}/series/" \
+276f"{xtream.authorization['username']}/" \
+277f"{xtream.authorization['password']}/{self.series_id}/"
297classXTream:
+ 298
+ 299name=""
+ 300server=""
+ 301secure_server=""
+ 302username=""
+ 303password=""
+ 304base_url=""
+ 305base_url_ssl="" 306
- 307state={'authenticated':False,'loaded':False}
+ 307cache_path="" 308
- 309hide_adult_content=False
+ 309account_expiration:timedelta 310
- 311live_catch_all_group=Group(
- 312{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},live_type
- 313)
- 314vod_catch_all_group=Group(
- 315{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},vod_type
- 316)
- 317series_catch_all_group=Group(
- 318{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},series_type
- 319)
- 320# If the cached JSON file is older than threshold_time_sec then load a new
- 321# JSON dictionary from the provider
- 322threshold_time_sec=-1
- 323
- 324def__init__(
- 325self,
- 326provider_name:str,
- 327provider_username:str,
- 328provider_password:str,
- 329provider_url:str,
- 330headers:dict=None,
- 331hide_adult_content:bool=False,
- 332cache_path:str="",
- 333reload_time_sec:int=60*60*8,
- 334validate_json:bool=False,
- 335debug_flask:bool=True
- 336):
- 337"""Initialize Xtream Class
- 338
- 339 Args:
- 340 provider_name (str): Name of the IPTV provider
- 341 provider_username (str): User name of the IPTV provider
- 342 provider_password (str): Password of the IPTV provider
- 343 provider_url (str): URL of the IPTV provider
- 344 headers (dict): Requests Headers
- 345 hide_adult_content(bool, optional): When `True` hide stream that are marked for adult
- 346 cache_path (str, optional): Location where to save loaded files.
- 347 Defaults to empty string.
- 348 reload_time_sec (int, optional): Number of seconds before automatic reloading
- 349 (-1 to turn it OFF)
- 350 debug_flask (bool, optional): Enable the debug mode in Flask
- 351 validate_json (bool, optional): Check Xtream API provided JSON for validity
- 352
- 353 Returns: XTream Class Instance
- 354
- 355 - Note 1: If it fails to authorize with provided username and password,
- 356 auth_data will be an empty dictionary.
- 357 - Note 2: The JSON validation option will take considerable amount of time and it should be
- 358 used only as a debug tool. The Xtream API JSON from the provider passes through a
- 359 schema that represent the best available understanding of how the Xtream API
- 360 works.
- 361 """
- 362self.server=provider_url
- 363self.username=provider_username
- 364self.password=provider_password
- 365self.name=provider_name
- 366self.cache_path=cache_path
- 367self.hide_adult_content=hide_adult_content
- 368self.threshold_time_sec=reload_time_sec
- 369self.validate_json=validate_json
- 370
- 371# get the pyxtream local path
- 372self.app_fullpath=osp.dirname(osp.realpath(__file__))
- 373
- 374# prepare location of local html template
- 375self.html_template_folder=osp.join(self.app_fullpath,"html")
- 376
- 377# if the cache_path is specified, test that it is a directory
- 378ifself.cache_path!="":
- 379# If the cache_path is not a directory, clear it
- 380ifnotosp.isdir(self.cache_path):
- 381print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
- 382self.cache_path==""
- 383
- 384# If the cache_path is still empty, use default
- 385ifself.cache_path=="":
- 386self.cache_path=osp.expanduser("~/.xtream-cache/")
- 387ifnotosp.isdir(self.cache_path):
- 388makedirs(self.cache_path,exist_ok=True)
- 389print(f"pyxtream cache path located at {self.cache_path}")
- 390
- 391ifheadersisnotNone:
- 392self.connection_headers=headers
- 393else:
- 394self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
- 395
- 396self.authenticate()
+ 311live_type="Live"
+ 312vod_type="VOD"
+ 313series_type="Series"
+ 314
+ 315auth_data={}
+ 316authorization={'username':'','password':''}
+ 317
+ 318groups=[]
+ 319channels=[]
+ 320series=[]
+ 321movies=[]
+ 322movies_30days=[]
+ 323movies_7days=[]
+ 324
+ 325connection_headers={}
+ 326
+ 327state={'authenticated':False,'loaded':False}
+ 328
+ 329hide_adult_content=False
+ 330
+ 331live_catch_all_group=Group(
+ 332{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},live_type
+ 333)
+ 334vod_catch_all_group=Group(
+ 335{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},vod_type
+ 336)
+ 337series_catch_all_group=Group(
+ 338{"category_id":"9999","category_name":"xEverythingElse","parent_id":0},series_type
+ 339)
+ 340# If the cached JSON file is older than threshold_time_sec then load a new
+ 341# JSON dictionary from the provider
+ 342threshold_time_sec=-1
+ 343
+ 344validate_json:bool=True
+ 345
+ 346# Used by REST API to get download progress
+ 347download_progress:dict={'StreamId':0,'Total':0,'Progress':0}
+ 348
+ 349def__init__(
+ 350self,
+ 351provider_name:str,
+ 352provider_username:str,
+ 353provider_password:str,
+ 354provider_url:str,
+ 355headers:dict=None,
+ 356hide_adult_content:bool=False,
+ 357cache_path:str="",
+ 358reload_time_sec:int=60*60*8,
+ 359validate_json:bool=False,
+ 360enable_flask:bool=False,
+ 361debug_flask:bool=True
+ 362):
+ 363"""Initialize Xtream Class
+ 364
+ 365 Args:
+ 366 provider_name (str): Name of the IPTV provider
+ 367 provider_username (str): User name of the IPTV provider
+ 368 provider_password (str): Password of the IPTV provider
+ 369 provider_url (str): URL of the IPTV provider
+ 370 headers (dict): Requests Headers
+ 371 hide_adult_content(bool, optional): When `True` hide stream that are marked for adult
+ 372 cache_path (str, optional): Location where to save loaded files.
+ 373 Defaults to empty string.
+ 374 reload_time_sec (int, optional): Number of seconds before automatic reloading
+ 375 (-1 to turn it OFF)
+ 376 validate_json (bool, optional): Check Xtream API provided JSON for validity
+ 377 enable_flask (bool, optional): Enable Flask
+ 378 debug_flask (bool, optional): Enable the debug mode in Flask
+ 379
+ 380 Returns: XTream Class Instance
+ 381
+ 382 - Note 1: If it fails to authorize with provided username and password,
+ 383 auth_data will be an empty dictionary.
+ 384 - Note 2: The JSON validation option will take considerable amount of time and it should be
+ 385 used only as a debug tool. The Xtream API JSON from the provider passes through a
+ 386 schema that represent the best available understanding of how the Xtream API
+ 387 works.
+ 388 """
+ 389self.server=provider_url
+ 390self.username=provider_username
+ 391self.password=provider_password
+ 392self.name=provider_name
+ 393self.cache_path=cache_path
+ 394self.hide_adult_content=hide_adult_content
+ 395self.threshold_time_sec=reload_time_sec
+ 396self.validate_json=validate_json 397
- 398ifself.threshold_time_sec>0:
- 399print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
- 400else:
- 401print("Reload timer is OFF")
- 402
- 403ifself.state['authenticated']:
- 404ifUSE_FLASK:
- 405self.flaskapp=FlaskWrap('pyxtream',self,self.html_template_folder,debug=debug_flask)
- 406self.flaskapp.start()
- 407
- 408defsearch_stream(self,keyword:str,
- 409ignore_case:bool=True,
- 410return_type:str="LIST",
- 411stream_type:list=("series","movies","channels"))->list:
- 412"""Search for streams
- 413
- 414 Args:
- 415 keyword (str): Keyword to search for. Supports REGEX
- 416 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
- 417 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
- 418 stream_type (list, optional): Search within specific stream type.
- 419
- 420 Returns:
- 421 list: List with all the results, it could be empty.
- 422 """
- 423
- 424search_result=[]
- 425regex_flags=re.IGNORECASEifignore_caseelse0
- 426regex=re.compile(keyword,regex_flags)
- 427# if ignore_case:
- 428# regex = re.compile(keyword, re.IGNORECASE)
- 429# else:
- 430# regex = re.compile(keyword)
- 431
- 432# if "movies" in stream_type:
- 433# print(f"Checking {len(self.movies)} movies")
- 434# for stream in self.movies:
- 435# if re.match(regex, stream.name) is not None:
- 436# search_result.append(stream.export_json())
- 437
- 438# if "channels" in stream_type:
- 439# print(f"Checking {len(self.channels)} channels")
- 440# for stream in self.channels:
- 441# if re.match(regex, stream.name) is not None:
- 442# search_result.append(stream.export_json())
+ 398# get the pyxtream local path
+ 399self.app_fullpath=osp.dirname(osp.realpath(__file__))
+ 400
+ 401# prepare location of local html template
+ 402self.html_template_folder=osp.join(self.app_fullpath,"html")
+ 403
+ 404# if the cache_path is specified, test that it is a directory
+ 405ifself.cache_path!="":
+ 406# If the cache_path is not a directory, clear it
+ 407ifnotosp.isdir(self.cache_path):
+ 408print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
+ 409self.cache_path=""
+ 410
+ 411# If the cache_path is still empty, use default
+ 412ifself.cache_path=="":
+ 413self.cache_path=osp.expanduser("~/.xtream-cache/")
+ 414ifnotosp.isdir(self.cache_path):
+ 415makedirs(self.cache_path,exist_ok=True)
+ 416print(f"pyxtream cache path located at {self.cache_path}")
+ 417
+ 418ifheadersisnotNone:
+ 419self.connection_headers=headers
+ 420else:
+ 421self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
+ 422
+ 423self.authenticate()
+ 424
+ 425ifself.threshold_time_sec>0:
+ 426print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
+ 427else:
+ 428print("Reload timer is OFF")
+ 429
+ 430ifself.state['authenticated']:
+ 431ifUSE_FLASKandenable_flask:
+ 432print("Starting Web Interface")
+ 433self.flaskapp=FlaskWrap(
+ 434'pyxtream',self,self.html_template_folder,debug=debug_flask
+ 435)
+ 436self.flaskapp.start()
+ 437else:
+ 438print("Web interface not running")
+ 439
+ 440defget_download_progress(self,stream_id:int=None):
+ 441# TODO: Add check for stream specific ID
+ 442returnjson.dumps(self.download_progress) 443
- 444# if "series" in stream_type:
- 445# print(f"Checking {len(self.series)} series")
- 446# for stream in self.series:
- 447# if re.match(regex, stream.name) is not None:
- 448# search_result.append(stream.export_json())
- 449
- 450stream_collections={
- 451"movies":self.movies,
- 452"channels":self.channels,
- 453"series":self.series
- 454}
- 455
- 456forstream_type_nameinstream_type:
- 457ifstream_type_nameinstream_collections:
- 458collection=stream_collections[stream_type_name]
- 459print(f"Checking {len(collection)}{stream_type_name}")
- 460forstreamincollection:
- 461ifre.match(regex,stream.name)isnotNone:
- 462search_result.append(stream.export_json())
- 463else:
- 464print(f"`{stream_type_name}` not found in collection")
- 465
- 466ifreturn_type=="JSON":
- 467# if search_result is not None:
- 468print(f"Found {len(search_result)} results `{keyword}`")
- 469returnjson.dumps(search_result,ensure_ascii=False)
- 470
- 471returnsearch_result
- 472
- 473defdownload_video(self,stream_id:int)->str:
- 474"""Download Video from Stream ID
- 475
- 476 Args:
- 477 stream_id (int): Stirng identifing the stream ID
- 478
- 479 Returns:
- 480 str: Absolute Path Filename where the file was saved. Empty if could not download
- 481 """
- 482url=""
- 483filename=""
- 484forstreaminself.movies:
- 485ifstream.id==stream_id:
- 486url=stream.url
- 487fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
- 488filename=osp.join(self.cache_path,fn)
+ 444defget_last_7days(self):
+ 445returnjson.dumps(self.movies_7days,default=lambdax:x.export_json())
+ 446
+ 447defsearch_stream(self,keyword:str,
+ 448ignore_case:bool=True,
+ 449return_type:str="LIST",
+ 450stream_type:list=("series","movies","channels"),
+ 451added_after:datetime=None)->list:
+ 452"""Search for streams
+ 453
+ 454 Args:
+ 455 keyword (str): Keyword to search for. Supports REGEX
+ 456 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
+ 457 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
+ 458 stream_type (list, optional): Search within specific stream type.
+ 459 added_after (datetime, optional): Search for items that have been added after a certain date.
+ 460
+ 461 Returns:
+ 462 list: List with all the results, it could be empty.
+ 463 """
+ 464
+ 465search_result=[]
+ 466regex_flags=re.IGNORECASEifignore_caseelse0
+ 467regex=re.compile(keyword,regex_flags)
+ 468
+ 469stream_collections={
+ 470"movies":self.movies,
+ 471"channels":self.channels,
+ 472"series":self.series
+ 473}
+ 474
+ 475forstream_type_nameinstream_type:
+ 476ifstream_type_nameinstream_collections:
+ 477collection=stream_collections[stream_type_name]
+ 478print(f"Checking {len(collection)}{stream_type_name}")
+ 479forstreamincollection:
+ 480ifstream.nameandre.match(regex,stream.name)isnotNone:
+ 481ifadded_afterisNone:
+ 482# Add all matches
+ 483search_result.append(stream.export_json())
+ 484else:
+ 485# Only add if it is more recent
+ 486pass
+ 487else:
+ 488print(f"`{stream_type_name}` not found in collection") 489
- 490# If the url was correctly built and file does not exists, start downloading
- 491ifurl!="":
- 492#if not osp.isfile(filename):
- 493ifnotself._download_video_impl(url,filename):
- 494return"Error"
- 495
- 496returnfilename
- 497
- 498def_download_video_impl(self,url:str,fullpath_filename:str)->bool:
- 499"""Download a stream
- 500
- 501 Args:
- 502 url (str): Complete URL of the stream
- 503 fullpath_filename (str): Complete File path where to save the stream
- 504
- 505 Returns:
- 506 bool: True if successful, False if error
- 507 """
- 508ret_code=False
- 509mb_size=1024*1024
- 510try:
- 511print(f"Downloading from URL `{url}` and saving at `{fullpath_filename}`")
- 512
- 513# Check if the file already exists
- 514ifosp.exists(fullpath_filename):
- 515# If the file exists, resume the download from where it left off
- 516file_size=osp.getsize(fullpath_filename)
- 517self.connection_headers['Range']=f'bytes={file_size}-'
- 518mode='ab'# Append to the existing file
- 519print(f"Resuming from {file_size:_} bytes")
- 520else:
- 521# If the file does not exist, start a new download
- 522mode='wb'# Write a new file
- 523
- 524# Make the request to download
- 525response=requests.get(url,timeout=(10),stream=True,allow_redirects=True,headers=self.connection_headers)
- 526# If there is an answer from the remote server
- 527ifresponse.status_code==200orresponse.status_code==206:
- 528# Get content type Binary or Text
- 529content_type=response.headers.get('content-type',None)
- 530
- 531# Get total playlist byte size
- 532total_content_size=int(response.headers.get('content-length',None))
- 533total_content_size_mb=total_content_size/mb_size
- 534
- 535# Set downloaded size
- 536downloaded_bytes=0
- 537
- 538# Set stream blocks
- 539block_bytes=int(4*mb_size)# 4 MB
- 540
- 541print(f"Ready to download {total_content_size_mb:.1f} MB file ({total_content_size})")
- 542ifcontent_type.split('/')[0]!="text":
- 543withopen(fullpath_filename,mode)asfile:
- 544
- 545# Grab data by block_bytes
- 546fordatainresponse.iter_content(block_bytes,decode_unicode=False):
- 547downloaded_bytes+=block_bytes
- 548progress(downloaded_bytes,total_content_size,"Downloading")
- 549file.write(data)
- 550
- 551ifdownloaded_bytes==total_content_size:
- 552ret_code=True
- 553
- 554# Delete Range if it was added
- 555try:
- 556delself.connection_headers['Range']
- 557exceptKeyError:
- 558pass
- 559else:
- 560print(f"URL has a file with unexpected content-type {content_type}")
- 561else:
- 562print(f"HTTP error {response.status_code} while retrieving from {url}")
- 563exceptExceptionase:
- 564print(e)
- 565
- 566returnret_code
- 567
- 568def_slugify(self,string:str)->str:
- 569"""Normalize string
- 570
- 571 Normalizes string, converts to lowercase, removes non-alpha characters,
- 572 and converts spaces to hyphens.
+ 490ifreturn_type=="JSON":
+ 491# if search_result is not None:
+ 492print(f"Found {len(search_result)} results `{keyword}`")
+ 493returnjson.dumps(search_result,ensure_ascii=False)
+ 494
+ 495returnsearch_result
+ 496
+ 497defdownload_video(self,stream_id:int)->str:
+ 498"""Download Video from Stream ID
+ 499
+ 500 Args:
+ 501 stream_id (int): String identifying the stream ID
+ 502
+ 503 Returns:
+ 504 str: Absolute Path Filename where the file was saved. Empty if could not download
+ 505 """
+ 506url=""
+ 507filename=""
+ 508forseries_streaminself.series:
+ 509ifseries_stream.series_id==stream_id:
+ 510episode_object:Episode=series_stream.episodes["1"]
+ 511url=f"{series_stream.url}/{episode_object.id}."\
+ 512f"{episode_object.container_extension}"
+ 513
+ 514forstreaminself.movies:
+ 515ifstream.id==stream_id:
+ 516url=stream.url
+ 517fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
+ 518filename=osp.join(self.cache_path,fn)
+ 519
+ 520# If the url was correctly built and file does not exists, start downloading
+ 521ifurl!="":
+ 522ifnotself._download_video_impl(url,filename):
+ 523return"Error"
+ 524
+ 525returnfilename
+ 526
+ 527def_download_video_impl(self,url:str,fullpath_filename:str)->bool:
+ 528"""Download a stream
+ 529
+ 530 Args:
+ 531 url (str): Complete URL of the stream
+ 532 fullpath_filename (str): Complete File path where to save the stream
+ 533
+ 534 Returns:
+ 535 bool: True if successful, False if error
+ 536 """
+ 537ret_code=False
+ 538mb_size=1024*1024
+ 539try:
+ 540print(f"Downloading from URL `{url}` and saving at `{fullpath_filename}`")
+ 541
+ 542# Check if the file already exists
+ 543ifosp.exists(fullpath_filename):
+ 544# If the file exists, resume the download from where it left off
+ 545file_size=osp.getsize(fullpath_filename)
+ 546self.connection_headers['Range']=f'bytes={file_size}-'
+ 547mode='ab'# Append to the existing file
+ 548print(f"Resuming from {file_size:_} bytes")
+ 549else:
+ 550# If the file does not exist, start a new download
+ 551mode='wb'# Write a new file
+ 552
+ 553# Make the request to download
+ 554response=requests.get(
+ 555url,timeout=(10),
+ 556stream=True,
+ 557allow_redirects=True,
+ 558headers=self.connection_headers
+ 559)
+ 560# If there is an answer from the remote server
+ 561ifresponse.status_codein(200,206):
+ 562# Get content type Binary or Text
+ 563content_type=response.headers.get('content-type',None)
+ 564
+ 565# Get total playlist byte size
+ 566total_content_size=int(response.headers.get('content-length',None))
+ 567total_content_size_mb=total_content_size/mb_size
+ 568
+ 569# Set downloaded size
+ 570downloaded_bytes=0
+ 571self.download_progress['Total']=total_content_size
+ 572self.download_progress['Progress']=0 573
- 574 Args:
- 575 string (str): String to be normalized
+ 574# Set stream blocks
+ 575block_bytes=int(4*mb_size)# 4 MB 576
- 577 Returns:
- 578 str: Normalized String
- 579 """
- 580return"".join(x.lower()forxinstringifx.isprintable())
- 581
- 582def_validate_url(self,url:str)->bool:
- 583regex=re.compile(
- 584r"^(?:http|ftp)s?://"# http:// or https://
- 585r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"# domain...
- 586r"localhost|"# localhost...
- 587r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"# ...or ip
- 588r"(?::\d+)?"# optional port
- 589r"(?:/?|[/?]\S+)$",
- 590re.IGNORECASE,
- 591)
- 592
- 593returnre.match(regex,url)isnotNone
- 594
- 595def_get_logo_local_path(self,logo_url:str)->str:
- 596"""Convert the Logo URL to a local Logo Path
- 597
- 598 Args:
- 599 logoURL (str): The Logo URL
- 600
- 601 Returns:
- 602 [type]: The logo path as a string or None
- 603 """
- 604local_logo_path=None
- 605iflogo_urlisnotNone:
- 606ifnotself._validate_url(logo_url):
- 607logo_url=None
- 608else:
- 609local_logo_path=osp.join(
- 610self.cache_path,
- 611f"{self._slugify(self.name)}-{self._slugify(osp.split(logo_url)[-1])}"
- 612)
- 613returnlocal_logo_path
+ 577print(
+ 578f"Ready to download {total_content_size_mb:.1f} MB file ({total_content_size})"
+ 579)
+ 580ifcontent_type.split('/')[0]!="text":
+ 581withopen(fullpath_filename,mode)asfile:
+ 582
+ 583# Grab data by block_bytes
+ 584fordatainresponse.iter_content(block_bytes,decode_unicode=False):
+ 585downloaded_bytes+=block_bytes
+ 586progress(downloaded_bytes,total_content_size,"Downloading")
+ 587self.download_progress['Progress']=downloaded_bytes
+ 588file.write(data)
+ 589
+ 590ret_code=True
+ 591
+ 592# Delete Range if it was added
+ 593try:
+ 594delself.connection_headers['Range']
+ 595exceptKeyError:
+ 596pass
+ 597else:
+ 598print(f"URL has a file with unexpected content-type {content_type}")
+ 599else:
+ 600print(f"HTTP error {response.status_code} while retrieving from {url}")
+ 601exceptrequests.exceptions.ReadTimeout:
+ 602print("Read Timeout, try again")
+ 603exceptExceptionase:
+ 604print("Unknown error")
+ 605print(e)
+ 606
+ 607returnret_code
+ 608
+ 609def_slugify(self,string:str)->str:
+ 610"""Normalize string
+ 611
+ 612 Normalizes string, converts to lowercase, removes non-alpha characters,
+ 613 and converts spaces to hyphens. 614
- 615defauthenticate(self):
- 616"""Login to provider"""
- 617# If we have not yet successfully authenticated, attempt authentication
- 618ifself.state["authenticated"]isFalse:
- 619# Erase any previous data
- 620self.auth_data={}
- 621# Loop through 30 seconds
- 622i=0
- 623r=None
- 624# Prepare the authentication url
- 625url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 626print("Attempting connection... ",end='')
- 627whilei<30:
- 628try:
- 629# Request authentication, wait 4 seconds maximum
- 630r=requests.get(url,timeout=(4),headers=self.connection_headers)
- 631i=31
- 632exceptrequests.exceptions.ConnectionError:
- 633time.sleep(1)
- 634print(f"{i} ",end='',flush=True)
- 635i+=1
- 636
- 637ifrisnotNone:
- 638# If the answer is ok, process data and change state
- 639ifr.ok:
- 640print("Connected")
- 641self.auth_data=r.json()
- 642self.authorization={
- 643"username":self.auth_data["user_info"]["username"],
- 644"password":self.auth_data["user_info"]["password"]
- 645}
- 646# Account expiration date
- 647self.account_expiration=timedelta(
- 648seconds=(
- 649int(self.auth_data["user_info"]["exp_date"])-datetime.now().timestamp()
- 650)
- 651)
- 652# Mark connection authorized
- 653self.state["authenticated"]=True
- 654# Construct the base url for all requests
- 655self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
- 656# If there is a secure server connection, construct the base url SSL for all requests
- 657if"https_port"inself.auth_data["server_info"]:
- 658self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
- 659f"/player_api.php?username={self.username}&password={self.password}"
- 660print(f"Account expires in {str(self.account_expiration)}")
- 661else:
- 662print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
- 663else:
- 664print(f"\n{self.name}: Provider refused the connection")
- 665
- 666def_load_from_file(self,filename)->dict:
- 667"""Try to load the dictionary from file
- 668
- 669 Args:
- 670 filename ([type]): File name containing the data
- 671
- 672 Returns:
- 673 dict: Dictionary if found and no errors, None if file does not exists
- 674 """
- 675# Build the full path
- 676full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 615 Args:
+ 616 string (str): String to be normalized
+ 617
+ 618 Returns:
+ 619 str: Normalized String
+ 620 """
+ 621return"".join(x.lower()forxinstringifx.isprintable())
+ 622
+ 623def_validate_url(self,url:str)->bool:
+ 624regex=re.compile(
+ 625r"^(?:http|ftp)s?://"# http:// or https://
+ 626r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"# domain...
+ 627r"localhost|"# localhost...
+ 628r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"# ...or ip
+ 629r"(?::\d+)?"# optional port
+ 630r"(?:/?|[/?]\S+)$",
+ 631re.IGNORECASE,
+ 632)
+ 633
+ 634returnre.match(regex,url)isnotNone
+ 635
+ 636def_get_logo_local_path(self,logo_url:str)->str:
+ 637"""Convert the Logo URL to a local Logo Path
+ 638
+ 639 Args:
+ 640 logoURL (str): The Logo URL
+ 641
+ 642 Returns:
+ 643 [type]: The logo path as a string or None
+ 644 """
+ 645local_logo_path=None
+ 646iflogo_urlisnotNone:
+ 647ifnotself._validate_url(logo_url):
+ 648logo_url=None
+ 649else:
+ 650local_logo_path=osp.join(
+ 651self.cache_path,
+ 652f"{self._slugify(self.name)}-{self._slugify(osp.split(logo_url)[-1])}"
+ 653)
+ 654returnlocal_logo_path
+ 655
+ 656defauthenticate(self):
+ 657"""Login to provider"""
+ 658# If we have not yet successfully authenticated, attempt authentication
+ 659ifself.state["authenticated"]isFalse:
+ 660# Erase any previous data
+ 661self.auth_data={}
+ 662# Loop through 30 seconds
+ 663i=0
+ 664r=None
+ 665# Prepare the authentication url
+ 666url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 667print("Attempting connection... ",end='')
+ 668whilei<30:
+ 669try:
+ 670# Request authentication, wait 4 seconds maximum
+ 671r=requests.get(url,timeout=(4),headers=self.connection_headers)
+ 672i=31
+ 673except(requests.exceptions.ConnectionError,requests.exceptions.ReadTimeout):
+ 674time.sleep(1)
+ 675print(f"{i} ",end='',flush=True)
+ 676i+=1 677
- 678# If the cached file exists, attempt to load it
- 679ifosp.isfile(full_filename):
- 680
- 681my_data=None
- 682
- 683# Get the enlapsed seconds since last file update
- 684file_age_sec=time.time()-osp.getmtime(full_filename)
- 685# If the file was updated less than the threshold time,
- 686# it means that the file is still fresh, we can load it.
- 687# Otherwise skip and return None to force a re-download
- 688ifself.threshold_time_sec>file_age_sec:
- 689# Load the JSON data
- 690try:
- 691withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
- 692my_data=json.load(myfile)
- 693iflen(my_data)==0:
- 694my_data=None
- 695exceptExceptionase:
- 696print(f" - Could not load from file `{full_filename}`: e=`{e}`")
- 697returnmy_data
- 698
- 699returnNone
- 700
- 701def_save_to_file(self,data_list:dict,filename:str)->bool:
- 702"""Save a dictionary to file
- 703
- 704 This function will overwrite the file if already exists
- 705
- 706 Args:
- 707 data_list (dict): Dictionary to save
- 708 filename (str): Name of the file
+ 678ifrisnotNone:
+ 679# If the answer is ok, process data and change state
+ 680ifr.ok:
+ 681print("Connected")
+ 682self.auth_data=r.json()
+ 683self.authorization={
+ 684"username":self.auth_data["user_info"]["username"],
+ 685"password":self.auth_data["user_info"]["password"]
+ 686}
+ 687# Account expiration date
+ 688self.account_expiration=timedelta(
+ 689seconds=(
+ 690int(self.auth_data["user_info"]["exp_date"])-datetime.now().timestamp()
+ 691)
+ 692)
+ 693# Mark connection authorized
+ 694self.state["authenticated"]=True
+ 695# Construct the base url for all requests
+ 696self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+ 697# If there is a secure server connection, construct the base url SSL for all requests
+ 698if"https_port"inself.auth_data["server_info"]:
+ 699self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
+ 700f"/player_api.php?username={self.username}&password={self.password}"
+ 701print(f"Account expires in {str(self.account_expiration)}")
+ 702else:
+ 703print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
+ 704else:
+ 705print(f"\n{self.name}: Provider refused the connection")
+ 706
+ 707def_load_from_file(self,filename)->dict:
+ 708"""Try to load the dictionary from file 709
- 710 Returns:
- 711 bool: True if successfull, False if error
- 712 """
- 713ifdata_listisNone:
- 714returnFalse
- 715
- 716full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
- 717try:
- 718withopen(full_filename,mode="wt",encoding="utf-8")asfile:
- 719json.dump(data_list,file,ensure_ascii=False)
- 720returnTrue
- 721exceptExceptionase:
- 722print(f" - Could not save to file `{full_filename}`: e=`{e}`")
- 723returnFalse
- 724# if data_list is not None:
- 725
- 726# #Build the full path
- 727# full_filename = osp.join(self.cache_path, f"{self._slugify(self.name)}-{filename}")
- 728# # If the path makes sense, save the file
- 729# json_data = json.dumps(data_list, ensure_ascii=False)
- 730# try:
- 731# with open(full_filename, mode="wt", encoding="utf-8") as myfile:
- 732# myfile.write(json_data)
- 733# except Exception as e:
- 734# print(f" - Could not save to file `{full_filename}`: e=`{e}`")
- 735# return False
- 736
- 737# return True
- 738# else:
- 739# return False
- 740
- 741defload_iptv(self)->bool:
- 742"""Load XTream IPTV
- 743
- 744 - Add all Live TV to XTream.channels
- 745 - Add all VOD to XTream.movies
- 746 - Add all Series to XTream.series
- 747 Series contains Seasons and Episodes. Those are not automatically
- 748 retrieved from the server to reduce the loading time.
- 749 - Add all groups to XTream.groups
- 750 Groups are for all three channel types, Live TV, VOD, and Series
- 751
- 752 Returns:
- 753 bool: True if successfull, False if error
- 754 """
- 755# If pyxtream has not authenticated the connection, return empty
- 756ifself.state["authenticated"]isFalse:
- 757print("Warning, cannot load steams since authorization failed")
- 758returnFalse
- 759
- 760# If pyxtream has already loaded the data, skip and return success
- 761ifself.state["loaded"]isTrue:
- 762print("Warning, data has already been loaded.")
- 763returnTrue
- 764
- 765forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
- 766## Get GROUPS
- 767
- 768# Try loading local file
- 769dt=0
- 770start=timer()
- 771all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
- 772# If file empty or does not exists, download it from remote
- 773ifall_catisNone:
- 774# Load all Groups and save file locally
- 775all_cat=self._load_categories_from_provider(loading_stream_type)
- 776ifall_catisnotNone:
- 777self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
- 778dt=timer()-start
- 779
- 780# If we got the GROUPS data, show the statistics and load GROUPS
- 781ifall_catisnotNone:
- 782print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
- 783## Add GROUPS to dictionaries
+ 710 Args:
+ 711 filename ([type]): File name containing the data
+ 712
+ 713 Returns:
+ 714 dict: Dictionary if found and no errors, None if file does not exists
+ 715 """
+ 716# Build the full path
+ 717full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 718
+ 719# If the cached file exists, attempt to load it
+ 720ifosp.isfile(full_filename):
+ 721
+ 722my_data=None
+ 723
+ 724# Get the elapsed seconds since last file update
+ 725file_age_sec=time.time()-osp.getmtime(full_filename)
+ 726# If the file was updated less than the threshold time,
+ 727# it means that the file is still fresh, we can load it.
+ 728# Otherwise skip and return None to force a re-download
+ 729ifself.threshold_time_sec>file_age_sec:
+ 730# Load the JSON data
+ 731try:
+ 732withopen(full_filename,mode="r",encoding="utf-8")asmyfile:
+ 733my_data=json.load(myfile)
+ 734iflen(my_data)==0:
+ 735my_data=None
+ 736exceptExceptionase:
+ 737print(f" - Could not load from file `{full_filename}`: e=`{e}`")
+ 738returnmy_data
+ 739
+ 740returnNone
+ 741
+ 742def_save_to_file(self,data_list:dict,filename:str)->bool:
+ 743"""Save a dictionary to file
+ 744
+ 745 This function will overwrite the file if already exists
+ 746
+ 747 Args:
+ 748 data_list (dict): Dictionary to save
+ 749 filename (str): Name of the file
+ 750
+ 751 Returns:
+ 752 bool: True if successful, False if error
+ 753 """
+ 754ifdata_listisNone:
+ 755returnFalse
+ 756
+ 757full_filename=osp.join(self.cache_path,f"{self._slugify(self.name)}-{filename}")
+ 758try:
+ 759withopen(full_filename,mode="wt",encoding="utf-8")asfile:
+ 760json.dump(data_list,file,ensure_ascii=False)
+ 761returnTrue
+ 762exceptExceptionase:
+ 763print(f" - Could not save to file `{full_filename}`: e=`{e}`")
+ 764returnFalse
+ 765
+ 766defload_iptv(self)->bool:
+ 767"""Load XTream IPTV
+ 768
+ 769 - Add all Live TV to XTream.channels
+ 770 - Add all VOD to XTream.movies
+ 771 - Add all Series to XTream.series
+ 772 Series contains Seasons and Episodes. Those are not automatically
+ 773 retrieved from the server to reduce the loading time.
+ 774 - Add all groups to XTream.groups
+ 775 Groups are for all three channel types, Live TV, VOD, and Series
+ 776
+ 777 Returns:
+ 778 bool: True if successful, False if error
+ 779 """
+ 780# If pyxtream has not authenticated the connection, return empty
+ 781ifself.state["authenticated"]isFalse:
+ 782print("Warning, cannot load steams since authorization failed")
+ 783returnFalse 784
- 785# Add the catch-all-errors group
- 786ifloading_stream_type==self.live_type:
- 787self.groups.append(self.live_catch_all_group)
- 788elifloading_stream_type==self.vod_type:
- 789self.groups.append(self.vod_catch_all_group)
- 790elifloading_stream_type==self.series_type:
- 791self.groups.append(self.series_catch_all_group)
- 792
- 793forcat_objinall_cat:
- 794ifschemaValidator(cat_obj,SchemaType.GROUP):
- 795# Create Group (Category)
- 796new_group=Group(cat_obj,loading_stream_type)
- 797# Add to xtream class
- 798self.groups.append(new_group)
- 799else:
- 800# Save what did not pass schema validation
- 801print(cat_obj)
- 802
- 803# Sort Categories
- 804self.groups.sort(key=lambdax:x.name)
- 805else:
- 806print(f" - Could not load {loading_stream_type} Groups")
- 807break
- 808
- 809## Get Streams
- 810
- 811# Try loading local file
- 812dt=0
- 813start=timer()
- 814all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
- 815# If file empty or does not exists, download it from remote
- 816ifall_streamsisNone:
- 817# Load all Streams and save file locally
- 818all_streams=self._load_streams_from_provider(loading_stream_type)
- 819self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
- 820dt=timer()-start
- 821
- 822# If we got the STREAMS data, show the statistics and load Streams
- 823ifall_streamsisnotNone:
- 824print(f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams in {dt:.3f} seconds")
- 825## Add Streams to dictionaries
+ 785# If pyxtream has already loaded the data, skip and return success
+ 786ifself.state["loaded"]isTrue:
+ 787print("Warning, data has already been loaded.")
+ 788returnTrue
+ 789
+ 790# Delete skipped channels from cache
+ 791full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 792try:
+ 793f=open(full_filename,mode="r+",encoding="utf-8")
+ 794f.truncate(0)
+ 795f.close()
+ 796exceptFileNotFoundError:
+ 797pass
+ 798
+ 799forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
+ 800# Get GROUPS
+ 801
+ 802# Try loading local file
+ 803dt=0
+ 804start=timer()
+ 805all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
+ 806# If file empty or does not exists, download it from remote
+ 807ifall_catisNone:
+ 808# Load all Groups and save file locally
+ 809all_cat=self._load_categories_from_provider(loading_stream_type)
+ 810ifall_catisnotNone:
+ 811self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
+ 812dt=timer()-start
+ 813
+ 814# If we got the GROUPS data, show the statistics and load GROUPS
+ 815ifall_catisnotNone:
+ 816print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
+ 817# Add GROUPS to dictionaries
+ 818
+ 819# Add the catch-all-errors group
+ 820ifloading_stream_type==self.live_type:
+ 821self.groups.append(self.live_catch_all_group)
+ 822elifloading_stream_type==self.vod_type:
+ 823self.groups.append(self.vod_catch_all_group)
+ 824elifloading_stream_type==self.series_type:
+ 825self.groups.append(self.series_catch_all_group) 826
- 827skipped_adult_content=0
- 828skipped_no_name_content=0
- 829
- 830number_of_streams=len(all_streams)
- 831current_stream_number=0
- 832# Calculate 1% of total number of streams
- 833# This is used to slow down the progress bar
- 834one_percent_number_of_streams=number_of_streams/100
- 835start=timer()
- 836forstream_channelinall_streams:
- 837skip_stream=False
- 838current_stream_number+=1
- 839
- 840# Show download progress every 1% of total number of streams
- 841ifcurrent_stream_number<one_percent_number_of_streams:
- 842progress(
- 843current_stream_number,
- 844number_of_streams,
- 845f"Processing {loading_stream_type} Streams"
- 846)
- 847one_percent_number_of_streams*=2
- 848
- 849# Validate JSON scheme
- 850ifself.validate_json:
- 851ifloading_stream_type==self.series_type:
- 852ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
- 853print(stream_channel)
- 854elifloading_stream_type==self.live_type:
- 855ifnotschemaValidator(stream_channel,SchemaType.LIVE):
- 856print(stream_channel)
- 857else:
- 858# vod_type
- 859ifnotschemaValidator(stream_channel,SchemaType.VOD):
- 860print(stream_channel)
- 861
- 862# Skip if the name of the stream is empty
- 863ifstream_channel["name"]=="":
- 864skip_stream=True
- 865skipped_no_name_content=skipped_no_name_content+1
- 866self._save_to_file_skipped_streams(stream_channel)
- 867
- 868# Skip if the user chose to hide adult streams
- 869ifself.hide_adult_contentandloading_stream_type==self.live_type:
- 870if"is_adult"instream_channel:
- 871ifstream_channel["is_adult"]=="1":
- 872skip_stream=True
- 873skipped_adult_content=skipped_adult_content+1
- 874self._save_to_file_skipped_streams(stream_channel)
- 875
- 876ifnotskip_stream:
- 877# Some channels have no group,
- 878# so let's add them to the catch all group
- 879ifstream_channel["category_id"]=="":
- 880stream_channel["category_id"]="9999"
- 881elifstream_channel["category_id"]!="1":
- 882pass
- 883
- 884# Find the first occurence of the group that the
- 885# Channel or Stream is pointing to
- 886the_group=next(
- 887(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
- 888None
- 889)
- 890
- 891# Set group title
- 892ifthe_groupisnotNone:
- 893group_title=the_group.name
- 894else:
- 895ifloading_stream_type==self.live_type:
- 896group_title=self.live_catch_all_group.name
- 897the_group=self.live_catch_all_group
- 898elifloading_stream_type==self.vod_type:
- 899group_title=self.vod_catch_all_group.name
- 900the_group=self.vod_catch_all_group
- 901elifloading_stream_type==self.series_type:
- 902group_title=self.series_catch_all_group.name
- 903the_group=self.series_catch_all_group
- 904
- 905
- 906ifloading_stream_type==self.series_type:
- 907# Load all Series
- 908new_series=Serie(self,stream_channel)
- 909# To get all the Episodes for every Season of each
- 910# Series is very time consuming, we will only
- 911# populate the Series once the user click on the
- 912# Series, the Seasons and Episodes will be loaded
- 913# using x.getSeriesInfoByID() function
- 914
- 915else:
- 916new_channel=Channel(
- 917self,
- 918group_title,
- 919stream_channel
- 920)
- 921
- 922ifnew_channel.group_id=="9999":
- 923print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+ 827forcat_objinall_cat:
+ 828ifschemaValidator(cat_obj,SchemaType.GROUP):
+ 829# Create Group (Category)
+ 830new_group=Group(cat_obj,loading_stream_type)
+ 831# Add to xtream class
+ 832self.groups.append(new_group)
+ 833else:
+ 834# Save what did not pass schema validation
+ 835print(cat_obj)
+ 836
+ 837# Sort Categories
+ 838self.groups.sort(key=lambdax:x.name)
+ 839else:
+ 840print(f" - Could not load {loading_stream_type} Groups")
+ 841break
+ 842
+ 843# Get Streams
+ 844
+ 845# Try loading local file
+ 846dt=0
+ 847start=timer()
+ 848all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
+ 849# If file empty or does not exists, download it from remote
+ 850ifall_streamsisNone:
+ 851# Load all Streams and save file locally
+ 852all_streams=self._load_streams_from_provider(loading_stream_type)
+ 853self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
+ 854dt=timer()-start
+ 855
+ 856# If we got the STREAMS data, show the statistics and load Streams
+ 857ifall_streamsisnotNone:
+ 858print(f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams in {dt:.3f} seconds")
+ 859# Add Streams to dictionaries
+ 860
+ 861skipped_adult_content=0
+ 862skipped_no_name_content=0
+ 863
+ 864number_of_streams=len(all_streams)
+ 865current_stream_number=0
+ 866# Calculate 1% of total number of streams
+ 867# This is used to slow down the progress bar
+ 868one_percent_number_of_streams=number_of_streams/100
+ 869start=timer()
+ 870forstream_channelinall_streams:
+ 871skip_stream=False
+ 872current_stream_number+=1
+ 873
+ 874# Show download progress every 1% of total number of streams
+ 875ifcurrent_stream_number<one_percent_number_of_streams:
+ 876progress(
+ 877current_stream_number,
+ 878number_of_streams,
+ 879f"Processing {loading_stream_type} Streams"
+ 880)
+ 881one_percent_number_of_streams*=2
+ 882
+ 883# Validate JSON scheme
+ 884ifself.validate_json:
+ 885ifloading_stream_type==self.series_type:
+ 886ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
+ 887print(stream_channel)
+ 888elifloading_stream_type==self.live_type:
+ 889ifnotschemaValidator(stream_channel,SchemaType.LIVE):
+ 890print(stream_channel)
+ 891else:
+ 892# vod_type
+ 893ifnotschemaValidator(stream_channel,SchemaType.VOD):
+ 894print(stream_channel)
+ 895
+ 896# Skip if the name of the stream is empty
+ 897ifstream_channel["name"]=="":
+ 898skip_stream=True
+ 899skipped_no_name_content=skipped_no_name_content+1
+ 900self._save_to_file_skipped_streams(stream_channel)
+ 901
+ 902# Skip if the user chose to hide adult streams
+ 903ifself.hide_adult_contentandloading_stream_type==self.live_type:
+ 904if"is_adult"instream_channel:
+ 905ifstream_channel["is_adult"]=="1":
+ 906skip_stream=True
+ 907skipped_adult_content=skipped_adult_content+1
+ 908self._save_to_file_skipped_streams(stream_channel)
+ 909
+ 910ifnotskip_stream:
+ 911# Some channels have no group,
+ 912# so let's add them to the catch all group
+ 913ifnotstream_channel["category_id"]:
+ 914stream_channel["category_id"]="9999"
+ 915elifstream_channel["category_id"]!="1":
+ 916pass
+ 917
+ 918# Find the first occurrence of the group that the
+ 919# Channel or Stream is pointing to
+ 920the_group=next(
+ 921(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
+ 922None
+ 923) 924
- 925# Save the new channel to the local list of channels
- 926ifloading_stream_type==self.live_type:
- 927self.channels.append(new_channel)
- 928elifloading_stream_type==self.vod_type:
- 929self.movies.append(new_channel)
- 930ifnew_channel.age_days_from_added<31:
- 931self.movies_30days.append(new_channel)
- 932ifnew_channel.age_days_from_added<7:
- 933self.movies_7days.append(new_channel)
- 934else:
- 935self.series.append(new_series)
- 936
- 937# Add stream to the specific Group
- 938ifthe_groupisnotNone:
- 939ifloading_stream_type!=self.series_type:
- 940the_group.channels.append(new_channel)
- 941else:
- 942the_group.series.append(new_series)
- 943else:
- 944print(f" - Group not found `{stream_channel['name']}`")
- 945print("\n")
- 946# Print information of which streams have been skipped
- 947ifself.hide_adult_content:
- 948print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
- 949ifskipped_no_name_content>0:
- 950print(f" - Skipped {skipped_no_name_content} "
- 951"unprintable {loading_stream_type} streams")
- 952else:
- 953print(f" - Could not load {loading_stream_type} Streams")
+ 925# Set group title
+ 926ifthe_groupisnotNone:
+ 927group_title=the_group.name
+ 928else:
+ 929ifloading_stream_type==self.live_type:
+ 930group_title=self.live_catch_all_group.name
+ 931the_group=self.live_catch_all_group
+ 932elifloading_stream_type==self.vod_type:
+ 933group_title=self.vod_catch_all_group.name
+ 934the_group=self.vod_catch_all_group
+ 935elifloading_stream_type==self.series_type:
+ 936group_title=self.series_catch_all_group.name
+ 937the_group=self.series_catch_all_group
+ 938
+ 939ifloading_stream_type==self.series_type:
+ 940# Load all Series
+ 941new_series=Serie(self,stream_channel)
+ 942# To get all the Episodes for every Season of each
+ 943# Series is very time consuming, we will only
+ 944# populate the Series once the user click on the
+ 945# Series, the Seasons and Episodes will be loaded
+ 946# using x.getSeriesInfoByID() function
+ 947
+ 948else:
+ 949new_channel=Channel(
+ 950self,
+ 951group_title,
+ 952stream_channel
+ 953) 954
- 955self.state["loaded"]=True
- 956
- 957def_save_to_file_skipped_streams(self,stream_channel:Channel):
- 958
- 959# Build the full path
- 960full_filename=osp.join(self.cache_path,"skipped_streams.json")
- 961
- 962# If the path makes sense, save the file
- 963json_data=json.dumps(stream_channel,ensure_ascii=False)
- 964try:
- 965withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
- 966myfile.writelines(json_data)
- 967returnTrue
- 968exceptExceptionase:
- 969print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
- 970returnFalse
- 971
- 972defget_series_info_by_id(self,get_series:dict):
- 973"""Get Seasons and Episodes for a Series
- 974
- 975 Args:
- 976 get_series (dict): Series dictionary
- 977 """
- 978
- 979series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
- 980
- 981ifseries_seasons["seasons"]isNone:
- 982series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
- 983
- 984forseries_infoinseries_seasons["seasons"]:
- 985season_name=series_info["name"]
- 986season_key=series_info['season_number']
- 987season=Season(season_name)
- 988get_series.seasons[season_name]=season
- 989if"episodes"inseries_seasons.keys():
- 990forseries_seasoninseries_seasons["episodes"].keys():
- 991forepisode_infoinseries_seasons["episodes"][str(series_season)]:
- 992new_episode_channel=Episode(
- 993self,series_info,"Testing",episode_info
- 994)
- 995season.episodes[episode_info["title"]]=new_episode_channel
- 996
- 997def_handle_request_exception(self,exception:requests.exceptions.RequestException):
- 998"""Handle different types of request exceptions."""
- 999ifisinstance(exception,requests.exceptions.ConnectionError):
-1000print(" - Connection Error: Possible network problem \
-1001 (e.g. DNS failure, refused connection, etc)")
-1002elifisinstance(exception,requests.exceptions.HTTPError):
-1003print(" - HTTP Error")
-1004elifisinstance(exception,requests.exceptions.TooManyRedirects):
-1005print(" - TooManyRedirects")
-1006elifisinstance(exception,requests.exceptions.ReadTimeout):
-1007print(" - Timeout while loading data")
-1008else:
-1009print(f" - An unexpected error occurred: {exception}")
-1010
-1011def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:
-1012"""Generic GET Request with Error handling
+ 955ifnew_channel.group_id=="9999":
+ 956print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+ 957
+ 958# Save the new channel to the local list of channels
+ 959ifloading_stream_type==self.live_type:
+ 960self.channels.append(new_channel)
+ 961elifloading_stream_type==self.vod_type:
+ 962self.movies.append(new_channel)
+ 963ifnew_channel.age_days_from_added<31:
+ 964self.movies_30days.append(new_channel)
+ 965ifnew_channel.age_days_from_added<7:
+ 966self.movies_7days.append(new_channel)
+ 967else:
+ 968self.series.append(new_series)
+ 969
+ 970# Add stream to the specific Group
+ 971ifthe_groupisnotNone:
+ 972ifloading_stream_type!=self.series_type:
+ 973the_group.channels.append(new_channel)
+ 974else:
+ 975the_group.series.append(new_series)
+ 976else:
+ 977print(f" - Group not found `{stream_channel['name']}`")
+ 978print("\n")
+ 979# Print information of which streams have been skipped
+ 980ifself.hide_adult_content:
+ 981print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
+ 982ifskipped_no_name_content>0:
+ 983print(f" - Skipped {skipped_no_name_content} "
+ 984"unprintable {loading_stream_type} streams")
+ 985else:
+ 986print(f" - Could not load {loading_stream_type} Streams")
+ 987
+ 988self.state["loaded"]=True
+ 989returnTrue
+ 990
+ 991def_save_to_file_skipped_streams(self,stream_channel:Channel):
+ 992
+ 993# Build the full path
+ 994full_filename=osp.join(self.cache_path,"skipped_streams.json")
+ 995
+ 996# If the path makes sense, save the file
+ 997json_data=json.dumps(stream_channel,ensure_ascii=False)
+ 998try:
+ 999withopen(full_filename,mode="a",encoding="utf-8")asmyfile:
+1000myfile.writelines(json_data)
+1001myfile.write('\n')
+1002returnTrue
+1003exceptExceptionase:
+1004print(f" - Could not save to skipped stream file `{full_filename}`: e=`{e}`")
+1005returnFalse
+1006
+1007defget_series_info_by_id(self,get_series:dict):
+1008"""Get Seasons and Episodes for a Series
+1009
+1010 Args:
+1011 get_series (dict): Series dictionary
+1012 """1013
-1014 Args:
-1015 URL (str): The URL where to GET content
-1016 timeout (Tuple[int, int], optional): Connection and Downloading Timeout.
-1017 Defaults to (2,15).
-1018
-1019 Returns:
-1020 Optional[dict]: JSON dictionary of the loaded data, or None
-1021 """
-1022forattemptinrange(10):
-1023time.sleep(1)
-1024try:
-1025response=requests.get(url,timeout=timeout,headers=self.connection_headers)
-1026response.raise_for_status()# Raise an HTTPError for bad responses (4xx and 5xx)
-1027returnresponse.json()
-1028exceptrequests.exceptions.RequestExceptionase:
-1029self._handle_request_exception(e)
-1030
-1031returnNone
-1032# i = 0
-1033# while i < 10:
-1034# time.sleep(1)
-1035# try:
-1036# r = requests.get(url, timeout=timeout, headers=self.connection_headers)
-1037# i = 20
-1038# if r.status_code == 200:
-1039# return r.json()
-1040# except requests.exceptions.ConnectionError:
-1041# print(" - Connection Error: Possible network problem (e.g. DNS failure, refused connection, etc)")
-1042# i += 1
-1043
-1044# except requests.exceptions.HTTPError:
-1045# print(" - HTTP Error")
-1046# i += 1
-1047
-1048# except requests.exceptions.TooManyRedirects:
-1049# print(" - TooManyRedirects")
-1050# i += 1
-1051
-1052# except requests.exceptions.ReadTimeout:
-1053# print(" - Timeout while loading data")
-1054# i += 1
-1055
-1056# return None
-1057
-1058# GET Stream Categories
-1059def_load_categories_from_provider(self,stream_type:str):
-1060"""Get from provider all category for specific stream type from provider
-1061
-1062 Args:
-1063 stream_type (str): Stream type can be Live, VOD, Series
-1064
-1065 Returns:
-1066 [type]: JSON if successfull, otherwise None
-1067 """
-1068url=""
-1069ifstream_type==self.live_type:
-1070url=self.get_live_categories_URL()
-1071elifstream_type==self.vod_type:
-1072url=self.get_vod_cat_URL()
-1073elifstream_type==self.series_type:
-1074url=self.get_series_cat_URL()
-1075else:
-1076url=""
-1077
-1078returnself._get_request(url)
-1079
-1080# GET Streams
-1081def_load_streams_from_provider(self,stream_type:str):
-1082"""Get from provider all streams for specific stream type
+1014series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
+1015
+1016ifseries_seasons["seasons"]isNone:
+1017series_seasons["seasons"]=[
+1018{"name":"Season 1","cover":series_seasons["info"]["cover"]}
+1019]
+1020
+1021forseries_infoinseries_seasons["seasons"]:
+1022season_name=series_info["name"]
+1023season=Season(season_name)
+1024get_series.seasons[season_name]=season
+1025if"episodes"inseries_seasons.keys():
+1026forseries_seasoninseries_seasons["episodes"].keys():
+1027forepisode_infoinseries_seasons["episodes"][str(series_season)]:
+1028new_episode_channel=Episode(
+1029self,series_info,"Testing",episode_info
+1030)
+1031season.episodes[episode_info["title"]]=new_episode_channel
+1032
+1033def_handle_request_exception(self,exception:requests.exceptions.RequestException):
+1034"""Handle different types of request exceptions."""
+1035ifisinstance(exception,requests.exceptions.ConnectionError):
+1036print(" - Connection Error: Possible network problem \
+1037 (e.g. DNS failure, refused connection, etc)")
+1038elifisinstance(exception,requests.exceptions.HTTPError):
+1039print(" - HTTP Error")
+1040elifisinstance(exception,requests.exceptions.TooManyRedirects):
+1041print(" - TooManyRedirects")
+1042elifisinstance(exception,requests.exceptions.ReadTimeout):
+1043print(" - Timeout while loading data")
+1044else:
+1045print(f" - An unexpected error occurred: {exception}")
+1046
+1047def_get_request(self,url:str,timeout:Tuple[int,int]=(2,15))->Optional[dict]:
+1048"""Generic GET Request with Error handling
+1049
+1050 Args:
+1051 URL (str): The URL where to GET content
+1052 timeout (Tuple[int, int], optional): Connection and Downloading Timeout.
+1053 Defaults to (2,15).
+1054
+1055 Returns:
+1056 Optional[dict]: JSON dictionary of the loaded data, or None
+1057 """
+1058
+1059kb_size=1024
+1060all_data=[]
+1061down_stats={"bytes":0,"kbytes":0,"mbytes":0,"start":0.0,"delta_sec":0.0}
+1062
+1063forattemptinrange(10):
+1064try:
+1065response=requests.get(
+1066url,
+1067stream=True,
+1068timeout=timeout,
+1069headers=self.connection_headers
+1070)
+1071response.raise_for_status()# Raise an HTTPError for bad responses (4xx and 5xx)
+1072break
+1073exceptrequests.exceptions.RequestExceptionase:
+1074self._handle_request_exception(e)
+1075returnNone
+1076
+1077# If there is an answer from the remote server
+1078ifresponse.status_codein(200,206):
+1079down_stats["start"]=time.perf_counter()
+1080
+1081# Set downloaded size
+1082down_stats["bytes"]=01083
-1084 Args:
-1085 stream_type (str): Stream type can be Live, VOD, Series
+1084# Set stream blocks
+1085block_bytes=int(1*kb_size*kb_size)# 4 MB1086
-1087 Returns:
-1088 [type]: JSON if successfull, otherwise None
-1089 """
-1090url=""
-1091ifstream_type==self.live_type:
-1092url=self.get_live_streams_URL()
-1093elifstream_type==self.vod_type:
-1094url=self.get_vod_streams_URL()
-1095elifstream_type==self.series_type:
-1096url=self.get_series_URL()
-1097else:
-1098url=""
-1099
-1100returnself._get_request(url)
-1101
-1102# GET Streams by Category
-1103def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
-1104"""Get from provider all streams for specific stream type with category/group ID
-1105
-1106 Args:
-1107 stream_type (str): Stream type can be Live, VOD, Series
-1108 category_id ([type]): Category/Group ID.
-1109
-1110 Returns:
-1111 [type]: JSON if successfull, otherwise None
-1112 """
-1113url=""
-1114
-1115ifstream_type==self.live_type:
-1116url=self.get_live_streams_URL_by_category(category_id)
-1117elifstream_type==self.vod_type:
-1118url=self.get_vod_streams_URL_by_category(category_id)
-1119elifstream_type==self.series_type:
-1120url=self.get_series_URL_by_category(category_id)
-1121else:
-1122url=""
-1123
-1124returnself._get_request(url)
-1125
-1126# GET SERIES Info
-1127def_load_series_info_by_id_from_provider(self,series_id:str):
-1128"""Gets informations about a Serie
-1129
-1130 Args:
-1131 series_id (str): Serie ID as described in Group
+1087# Grab data by block_bytes
+1088fordatainresponse.iter_content(block_bytes,decode_unicode=False):
+1089down_stats["bytes"]+=len(data)
+1090down_stats["kbytes"]=down_stats["bytes"]/kb_size
+1091down_stats["mbytes"]=down_stats["bytes"]/kb_size/kb_size
+1092down_stats["delta_sec"]=time.perf_counter()-down_stats["start"]
+1093download_speed_average=down_stats["kbytes"]//down_stats["delta_sec"]
+1094sys.stdout.write(
+1095f'\rDownloading {down_stats["kbytes"]:.1f} MB at {download_speed_average:.0f} kB/s'
+1096)
+1097sys.stdout.flush()
+1098all_data.append(data)
+1099print(" - Done")
+1100full_content=b''.join(all_data)
+1101returnjson.loads(full_content)
+1102
+1103print(f"HTTP error {response.status_code} while retrieving from {url}")
+1104
+1105returnNone
+1106
+1107# GET Stream Categories
+1108def_load_categories_from_provider(self,stream_type:str):
+1109"""Get from provider all category for specific stream type from provider
+1110
+1111 Args:
+1112 stream_type (str): Stream type can be Live, VOD, Series
+1113
+1114 Returns:
+1115 [type]: JSON if successful, otherwise None
+1116 """
+1117url=""
+1118ifstream_type==self.live_type:
+1119url=self.get_live_categories_URL()
+1120elifstream_type==self.vod_type:
+1121url=self.get_vod_cat_URL()
+1122elifstream_type==self.series_type:
+1123url=self.get_series_cat_URL()
+1124else:
+1125url=""
+1126
+1127returnself._get_request(url)
+1128
+1129# GET Streams
+1130def_load_streams_from_provider(self,stream_type:str):
+1131"""Get from provider all streams for specific stream type1132
-1133 Returns:
-1134 [type]: JSON if successfull, otherwise None
-1135 """
-1136returnself._get_request(self.get_series_info_URL_by_ID(series_id))
-1137
-1138# The seasons array, might be filled or might be completely empty.
-1139# If it is not empty, it will contain the cover, overview and the air date
-1140# of the selected season.
-1141# In your APP if you want to display the series, you have to take that
-1142# from the episodes array.
-1143
-1144# GET VOD Info
-1145defvodInfoByID(self,vod_id):
-1146returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
-1147
-1148# GET short_epg for LIVE Streams (same as stalker portal,
-1149# prints the next X EPG that will play soon)
-1150defliveEpgByStream(self,stream_id):
-1151returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
-1152
-1153defliveEpgByStreamAndLimit(self,stream_id,limit):
-1154returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
-1155
-1156# GET ALL EPG for LIVE Streams (same as stalker portal,
-1157# but it will print all epg listings regardless of the day)
-1158defallLiveEpgByStream(self,stream_id):
-1159returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
-1160
-1161# Full EPG List for all Streams
-1162defallEpg(self):
-1163returnself._get_request(self.get_all_epg_URL())
-1164
-1165## URL-builder methods
-1166defget_live_categories_URL(self)->str:
-1167returnf"{self.base_url}&action=get_live_categories"
-1168
-1169defget_live_streams_URL(self)->str:
-1170returnf"{self.base_url}&action=get_live_streams"
-1171
-1172defget_live_streams_URL_by_category(self,category_id)->str:
-1173returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
+1133 Args:
+1134 stream_type (str): Stream type can be Live, VOD, Series
+1135
+1136 Returns:
+1137 [type]: JSON if successful, otherwise None
+1138 """
+1139url=""
+1140ifstream_type==self.live_type:
+1141url=self.get_live_streams_URL()
+1142elifstream_type==self.vod_type:
+1143url=self.get_vod_streams_URL()
+1144elifstream_type==self.series_type:
+1145url=self.get_series_URL()
+1146else:
+1147url=""
+1148
+1149returnself._get_request(url)
+1150
+1151# GET Streams by Category
+1152def_load_streams_by_category_from_provider(self,stream_type:str,category_id):
+1153"""Get from provider all streams for specific stream type with category/group ID
+1154
+1155 Args:
+1156 stream_type (str): Stream type can be Live, VOD, Series
+1157 category_id ([type]): Category/Group ID.
+1158
+1159 Returns:
+1160 [type]: JSON if successful, otherwise None
+1161 """
+1162url=""
+1163
+1164ifstream_type==self.live_type:
+1165url=self.get_live_streams_URL_by_category(category_id)
+1166elifstream_type==self.vod_type:
+1167url=self.get_vod_streams_URL_by_category(category_id)
+1168elifstream_type==self.series_type:
+1169url=self.get_series_URL_by_category(category_id)
+1170else:
+1171url=""
+1172
+1173returnself._get_request(url)1174
-1175defget_vod_cat_URL(self)->str:
-1176returnf"{self.base_url}&action=get_vod_categories"
-1177
-1178defget_vod_streams_URL(self)->str:
-1179returnf"{self.base_url}&action=get_vod_streams"
-1180
-1181defget_vod_streams_URL_by_category(self,category_id)->str:
-1182returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
-1183
-1184defget_series_cat_URL(self)->str:
-1185returnf"{self.base_url}&action=get_series_categories"
-1186
-1187defget_series_URL(self)->str:
-1188returnf"{self.base_url}&action=get_series"
-1189
-1190defget_series_URL_by_category(self,category_id)->str:
-1191returnf"{self.base_url}&action=get_series&category_id={category_id}"
-1192
-1193defget_series_info_URL_by_ID(self,series_id)->str:
-1194returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
-1195
-1196defget_VOD_info_URL_by_ID(self,vod_id)->str:
-1197returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
-1198
-1199defget_live_epg_URL_by_stream(self,stream_id)->str:
-1200returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
-1201
-1202defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
-1203returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
-1204
-1205defget_all_live_epg_URL_by_stream(self,stream_id)->str:
-1206returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
-1207
-1208defget_all_epg_URL(self)->str:
-1209returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
+1175# GET SERIES Info
+1176def_load_series_info_by_id_from_provider(self,series_id:str,return_type:str="DICT"):
+1177"""Gets information about a Serie
+1178
+1179 Args:
+1180 series_id (str): Serie ID as described in Group
+1181 return_type (str, optional): Output format, 'DICT' or 'JSON'. Defaults to "DICT".
+1182
+1183 Returns:
+1184 [type]: JSON if successful, otherwise None
+1185 """
+1186data=self._get_request(self.get_series_info_URL_by_ID(series_id))
+1187ifreturn_type=="JSON":
+1188returnjson.dumps(data,ensure_ascii=False)
+1189returndata
+1190
+1191# The seasons array, might be filled or might be completely empty.
+1192# If it is not empty, it will contain the cover, overview and the air date
+1193# of the selected season.
+1194# In your APP if you want to display the series, you have to take that
+1195# from the episodes array.
+1196
+1197# GET VOD Info
+1198defvodInfoByID(self,vod_id):
+1199returnself._get_request(self.get_VOD_info_URL_by_ID(vod_id))
+1200
+1201# GET short_epg for LIVE Streams (same as stalker portal,
+1202# prints the next X EPG that will play soon)
+1203defliveEpgByStream(self,stream_id):
+1204returnself._get_request(self.get_live_epg_URL_by_stream(stream_id))
+1205
+1206defliveEpgByStreamAndLimit(self,stream_id,limit):
+1207returnself._get_request(self.get_live_epg_URL_by_stream_and_limit(stream_id,limit))
+1208
+1209# GET ALL EPG for LIVE Streams (same as stalker portal,
+1210# but it will print all epg listings regardless of the day)
+1211defallLiveEpgByStream(self,stream_id):
+1212returnself._get_request(self.get_all_live_epg_URL_by_stream(stream_id))
+1213
+1214# Full EPG List for all Streams
+1215defallEpg(self):
+1216returnself._get_request(self.get_all_epg_URL())
+1217
+1218# URL-builder methods
+1219defget_live_categories_URL(self)->str:
+1220returnf"{self.base_url}&action=get_live_categories"
+1221
+1222defget_live_streams_URL(self)->str:
+1223returnf"{self.base_url}&action=get_live_streams"
+1224
+1225defget_live_streams_URL_by_category(self,category_id)->str:
+1226returnf"{self.base_url}&action=get_live_streams&category_id={category_id}"
+1227
+1228defget_vod_cat_URL(self)->str:
+1229returnf"{self.base_url}&action=get_vod_categories"
+1230
+1231defget_vod_streams_URL(self)->str:
+1232returnf"{self.base_url}&action=get_vod_streams"
+1233
+1234defget_vod_streams_URL_by_category(self,category_id)->str:
+1235returnf"{self.base_url}&action=get_vod_streams&category_id={category_id}"
+1236
+1237defget_series_cat_URL(self)->str:
+1238returnf"{self.base_url}&action=get_series_categories"
+1239
+1240defget_series_URL(self)->str:
+1241returnf"{self.base_url}&action=get_series"
+1242
+1243defget_series_URL_by_category(self,category_id)->str:
+1244returnf"{self.base_url}&action=get_series&category_id={category_id}"
+1245
+1246defget_series_info_URL_by_ID(self,series_id)->str:
+1247returnf"{self.base_url}&action=get_series_info&series_id={series_id}"
+1248
+1249defget_VOD_info_URL_by_ID(self,vod_id)->str:
+1250returnf"{self.base_url}&action=get_vod_info&vod_id={vod_id}"
+1251
+1252defget_live_epg_URL_by_stream(self,stream_id)->str:
+1253returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}"
+1254
+1255defget_live_epg_URL_by_stream_and_limit(self,stream_id,limit)->str:
+1256returnf"{self.base_url}&action=get_short_epg&stream_id={stream_id}&limit={limit}"
+1257
+1258defget_all_live_epg_URL_by_stream(self,stream_id)->str:
+1259returnf"{self.base_url}&action=get_simple_data_table&stream_id={stream_id}"
+1260
+1261defget_all_epg_URL(self)->str:
+1262returnf"{self.server}/xmltv.php?username={self.username}&password={self.password}"
324def__init__(
-325self,
-326provider_name:str,
-327provider_username:str,
-328provider_password:str,
-329provider_url:str,
-330headers:dict=None,
-331hide_adult_content:bool=False,
-332cache_path:str="",
-333reload_time_sec:int=60*60*8,
-334validate_json:bool=False,
-335debug_flask:bool=True
-336):
-337"""Initialize Xtream Class
-338
-339 Args:
-340 provider_name (str): Name of the IPTV provider
-341 provider_username (str): User name of the IPTV provider
-342 provider_password (str): Password of the IPTV provider
-343 provider_url (str): URL of the IPTV provider
-344 headers (dict): Requests Headers
-345 hide_adult_content(bool, optional): When `True` hide stream that are marked for adult
-346 cache_path (str, optional): Location where to save loaded files.
-347 Defaults to empty string.
-348 reload_time_sec (int, optional): Number of seconds before automatic reloading
-349 (-1 to turn it OFF)
-350 debug_flask (bool, optional): Enable the debug mode in Flask
-351 validate_json (bool, optional): Check Xtream API provided JSON for validity
-352
-353 Returns: XTream Class Instance
-354
-355 - Note 1: If it fails to authorize with provided username and password,
-356 auth_data will be an empty dictionary.
-357 - Note 2: The JSON validation option will take considerable amount of time and it should be
-358 used only as a debug tool. The Xtream API JSON from the provider passes through a
-359 schema that represent the best available understanding of how the Xtream API
-360 works.
-361 """
-362self.server=provider_url
-363self.username=provider_username
-364self.password=provider_password
-365self.name=provider_name
-366self.cache_path=cache_path
-367self.hide_adult_content=hide_adult_content
-368self.threshold_time_sec=reload_time_sec
-369self.validate_json=validate_json
-370
-371# get the pyxtream local path
-372self.app_fullpath=osp.dirname(osp.realpath(__file__))
-373
-374# prepare location of local html template
-375self.html_template_folder=osp.join(self.app_fullpath,"html")
-376
-377# if the cache_path is specified, test that it is a directory
-378ifself.cache_path!="":
-379# If the cache_path is not a directory, clear it
-380ifnotosp.isdir(self.cache_path):
-381print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
-382self.cache_path==""
-383
-384# If the cache_path is still empty, use default
-385ifself.cache_path=="":
-386self.cache_path=osp.expanduser("~/.xtream-cache/")
-387ifnotosp.isdir(self.cache_path):
-388makedirs(self.cache_path,exist_ok=True)
-389print(f"pyxtream cache path located at {self.cache_path}")
-390
-391ifheadersisnotNone:
-392self.connection_headers=headers
-393else:
-394self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
-395
-396self.authenticate()
+
349def__init__(
+350self,
+351provider_name:str,
+352provider_username:str,
+353provider_password:str,
+354provider_url:str,
+355headers:dict=None,
+356hide_adult_content:bool=False,
+357cache_path:str="",
+358reload_time_sec:int=60*60*8,
+359validate_json:bool=False,
+360enable_flask:bool=False,
+361debug_flask:bool=True
+362):
+363"""Initialize Xtream Class
+364
+365 Args:
+366 provider_name (str): Name of the IPTV provider
+367 provider_username (str): User name of the IPTV provider
+368 provider_password (str): Password of the IPTV provider
+369 provider_url (str): URL of the IPTV provider
+370 headers (dict): Requests Headers
+371 hide_adult_content(bool, optional): When `True` hide stream that are marked for adult
+372 cache_path (str, optional): Location where to save loaded files.
+373 Defaults to empty string.
+374 reload_time_sec (int, optional): Number of seconds before automatic reloading
+375 (-1 to turn it OFF)
+376 validate_json (bool, optional): Check Xtream API provided JSON for validity
+377 enable_flask (bool, optional): Enable Flask
+378 debug_flask (bool, optional): Enable the debug mode in Flask
+379
+380 Returns: XTream Class Instance
+381
+382 - Note 1: If it fails to authorize with provided username and password,
+383 auth_data will be an empty dictionary.
+384 - Note 2: The JSON validation option will take considerable amount of time and it should be
+385 used only as a debug tool. The Xtream API JSON from the provider passes through a
+386 schema that represent the best available understanding of how the Xtream API
+387 works.
+388 """
+389self.server=provider_url
+390self.username=provider_username
+391self.password=provider_password
+392self.name=provider_name
+393self.cache_path=cache_path
+394self.hide_adult_content=hide_adult_content
+395self.threshold_time_sec=reload_time_sec
+396self.validate_json=validate_json397
-398ifself.threshold_time_sec>0:
-399print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
-400else:
-401print("Reload timer is OFF")
-402
-403ifself.state['authenticated']:
-404ifUSE_FLASK:
-405self.flaskapp=FlaskWrap('pyxtream',self,self.html_template_folder,debug=debug_flask)
-406self.flaskapp.start()
+398# get the pyxtream local path
+399self.app_fullpath=osp.dirname(osp.realpath(__file__))
+400
+401# prepare location of local html template
+402self.html_template_folder=osp.join(self.app_fullpath,"html")
+403
+404# if the cache_path is specified, test that it is a directory
+405ifself.cache_path!="":
+406# If the cache_path is not a directory, clear it
+407ifnotosp.isdir(self.cache_path):
+408print(" - Cache Path is not a directory, using default '~/.xtream-cache/'")
+409self.cache_path=""
+410
+411# If the cache_path is still empty, use default
+412ifself.cache_path=="":
+413self.cache_path=osp.expanduser("~/.xtream-cache/")
+414ifnotosp.isdir(self.cache_path):
+415makedirs(self.cache_path,exist_ok=True)
+416print(f"pyxtream cache path located at {self.cache_path}")
+417
+418ifheadersisnotNone:
+419self.connection_headers=headers
+420else:
+421self.connection_headers={'User-Agent':"Wget/1.20.3 (linux-gnu)"}
+422
+423self.authenticate()
+424
+425ifself.threshold_time_sec>0:
+426print(f"Reload timer is ON and set to {self.threshold_time_sec} seconds")
+427else:
+428print("Reload timer is OFF")
+429
+430ifself.state['authenticated']:
+431ifUSE_FLASKandenable_flask:
+432print("Starting Web Interface")
+433self.flaskapp=FlaskWrap(
+434'pyxtream',self,self.html_template_folder,debug=debug_flask
+435)
+436self.flaskapp.start()
+437else:
+438print("Web interface not running")
@@ -3885,17 +4038,18 @@
Defaults to empty string.
reload_time_sec (int, optional): Number of seconds before automatic reloading
(-1 to turn it OFF)
- debug_flask (bool, optional): Enable the debug mode in Flask
- validate_json (bool, optional): Check Xtream API provided JSON for validity
+ validate_json (bool, optional): Check Xtream API provided JSON for validity
+ enable_flask (bool, optional): Enable Flask
+ debug_flask (bool, optional): Enable the debug mode in Flask
Returns: XTream Class Instance
Note 1: If it fails to authorize with provided username and password,
auth_data will be an empty dictionary.
-
Note 2: The JSON validation option will take considerable amount of time and it should be
+
Note 2: The JSON validation option will take considerable amount of time and it should be
used only as a debug tool. The Xtream API JSON from the provider passes through a
-schema that represent the best available understanding of how the Xtream API
+schema that represent the best available understanding of how the Xtream API
works.
408defsearch_stream(self,keyword:str,
-409ignore_case:bool=True,
-410return_type:str="LIST",
-411stream_type:list=("series","movies","channels"))->list:
-412"""Search for streams
-413
-414 Args:
-415 keyword (str): Keyword to search for. Supports REGEX
-416 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
-417 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
-418 stream_type (list, optional): Search within specific stream type.
-419
-420 Returns:
-421 list: List with all the results, it could be empty.
-422 """
-423
-424search_result=[]
-425regex_flags=re.IGNORECASEifignore_caseelse0
-426regex=re.compile(keyword,regex_flags)
-427# if ignore_case:
-428# regex = re.compile(keyword, re.IGNORECASE)
-429# else:
-430# regex = re.compile(keyword)
-431
-432# if "movies" in stream_type:
-433# print(f"Checking {len(self.movies)} movies")
-434# for stream in self.movies:
-435# if re.match(regex, stream.name) is not None:
-436# search_result.append(stream.export_json())
-437
-438# if "channels" in stream_type:
-439# print(f"Checking {len(self.channels)} channels")
-440# for stream in self.channels:
-441# if re.match(regex, stream.name) is not None:
-442# search_result.append(stream.export_json())
-443
-444# if "series" in stream_type:
-445# print(f"Checking {len(self.series)} series")
-446# for stream in self.series:
-447# if re.match(regex, stream.name) is not None:
-448# search_result.append(stream.export_json())
-449
-450stream_collections={
-451"movies":self.movies,
-452"channels":self.channels,
-453"series":self.series
-454}
-455
-456forstream_type_nameinstream_type:
-457ifstream_type_nameinstream_collections:
-458collection=stream_collections[stream_type_name]
-459print(f"Checking {len(collection)}{stream_type_name}")
-460forstreamincollection:
-461ifre.match(regex,stream.name)isnotNone:
-462search_result.append(stream.export_json())
-463else:
-464print(f"`{stream_type_name}` not found in collection")
-465
-466ifreturn_type=="JSON":
-467# if search_result is not None:
-468print(f"Found {len(search_result)} results `{keyword}`")
-469returnjson.dumps(search_result,ensure_ascii=False)
-470
-471returnsearch_result
+
447defsearch_stream(self,keyword:str,
+448ignore_case:bool=True,
+449return_type:str="LIST",
+450stream_type:list=("series","movies","channels"),
+451added_after:datetime=None)->list:
+452"""Search for streams
+453
+454 Args:
+455 keyword (str): Keyword to search for. Supports REGEX
+456 ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
+457 return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
+458 stream_type (list, optional): Search within specific stream type.
+459 added_after (datetime, optional): Search for items that have been added after a certain date.
+460
+461 Returns:
+462 list: List with all the results, it could be empty.
+463 """
+464
+465search_result=[]
+466regex_flags=re.IGNORECASEifignore_caseelse0
+467regex=re.compile(keyword,regex_flags)
+468
+469stream_collections={
+470"movies":self.movies,
+471"channels":self.channels,
+472"series":self.series
+473}
+474
+475forstream_type_nameinstream_type:
+476ifstream_type_nameinstream_collections:
+477collection=stream_collections[stream_type_name]
+478print(f"Checking {len(collection)}{stream_type_name}")
+479forstreamincollection:
+480ifstream.nameandre.match(regex,stream.name)isnotNone:
+481ifadded_afterisNone:
+482# Add all matches
+483search_result.append(stream.export_json())
+484else:
+485# Only add if it is more recent
+486pass
+487else:
+488print(f"`{stream_type_name}` not found in collection")
+489
+490ifreturn_type=="JSON":
+491# if search_result is not None:
+492print(f"Found {len(search_result)} results `{keyword}`")
+493returnjson.dumps(search_result,ensure_ascii=False)
+494
+495returnsearch_result
@@ -4306,7 +4533,8 @@
keyword (str): Keyword to search for. Supports REGEX
ignore_case (bool, optional): True to ignore case during search. Defaults to "True".
return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to "LIST".
- stream_type (list, optional): Search within specific stream type.
+ stream_type (list, optional): Search within specific stream type.
+ added_after (datetime, optional): Search for items that have been added after a certain date.
Returns:
list: List with all the results, it could be empty.
@@ -4325,37 +4553,42 @@
-
473defdownload_video(self,stream_id:int)->str:
-474"""Download Video from Stream ID
-475
-476 Args:
-477 stream_id (int): Stirng identifing the stream ID
-478
-479 Returns:
-480 str: Absolute Path Filename where the file was saved. Empty if could not download
-481 """
-482url=""
-483filename=""
-484forstreaminself.movies:
-485ifstream.id==stream_id:
-486url=stream.url
-487fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
-488filename=osp.join(self.cache_path,fn)
-489
-490# If the url was correctly built and file does not exists, start downloading
-491ifurl!="":
-492#if not osp.isfile(filename):
-493ifnotself._download_video_impl(url,filename):
-494return"Error"
-495
-496returnfilename
+
497defdownload_video(self,stream_id:int)->str:
+498"""Download Video from Stream ID
+499
+500 Args:
+501 stream_id (int): String identifying the stream ID
+502
+503 Returns:
+504 str: Absolute Path Filename where the file was saved. Empty if could not download
+505 """
+506url=""
+507filename=""
+508forseries_streaminself.series:
+509ifseries_stream.series_id==stream_id:
+510episode_object:Episode=series_stream.episodes["1"]
+511url=f"{series_stream.url}/{episode_object.id}."\
+512f"{episode_object.container_extension}"
+513
+514forstreaminself.movies:
+515ifstream.id==stream_id:
+516url=stream.url
+517fn=f"{self._slugify(stream.name)}.{stream.raw['container_extension']}"
+518filename=osp.join(self.cache_path,fn)
+519
+520# If the url was correctly built and file does not exists, start downloading
+521ifurl!="":
+522ifnotself._download_video_impl(url,filename):
+523return"Error"
+524
+525returnfilename
Download Video from Stream ID
Args:
- stream_id (int): Stirng identifing the stream ID
+ stream_id (int): String identifying the stream ID
Returns:
str: Absolute Path Filename where the file was saved. Empty if could not download
@@ -4374,56 +4607,56 @@
-
615defauthenticate(self):
-616"""Login to provider"""
-617# If we have not yet successfully authenticated, attempt authentication
-618ifself.state["authenticated"]isFalse:
-619# Erase any previous data
-620self.auth_data={}
-621# Loop through 30 seconds
-622i=0
-623r=None
-624# Prepare the authentication url
-625url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
-626print("Attempting connection... ",end='')
-627whilei<30:
-628try:
-629# Request authentication, wait 4 seconds maximum
-630r=requests.get(url,timeout=(4),headers=self.connection_headers)
-631i=31
-632exceptrequests.exceptions.ConnectionError:
-633time.sleep(1)
-634print(f"{i} ",end='',flush=True)
-635i+=1
-636
-637ifrisnotNone:
-638# If the answer is ok, process data and change state
-639ifr.ok:
-640print("Connected")
-641self.auth_data=r.json()
-642self.authorization={
-643"username":self.auth_data["user_info"]["username"],
-644"password":self.auth_data["user_info"]["password"]
-645}
-646# Account expiration date
-647self.account_expiration=timedelta(
-648seconds=(
-649int(self.auth_data["user_info"]["exp_date"])-datetime.now().timestamp()
-650)
-651)
-652# Mark connection authorized
-653self.state["authenticated"]=True
-654# Construct the base url for all requests
-655self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
-656# If there is a secure server connection, construct the base url SSL for all requests
-657if"https_port"inself.auth_data["server_info"]:
-658self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
-659f"/player_api.php?username={self.username}&password={self.password}"
-660print(f"Account expires in {str(self.account_expiration)}")
-661else:
-662print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
-663else:
-664print(f"\n{self.name}: Provider refused the connection")
+
656defauthenticate(self):
+657"""Login to provider"""
+658# If we have not yet successfully authenticated, attempt authentication
+659ifself.state["authenticated"]isFalse:
+660# Erase any previous data
+661self.auth_data={}
+662# Loop through 30 seconds
+663i=0
+664r=None
+665# Prepare the authentication url
+666url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+667print("Attempting connection... ",end='')
+668whilei<30:
+669try:
+670# Request authentication, wait 4 seconds maximum
+671r=requests.get(url,timeout=(4),headers=self.connection_headers)
+672i=31
+673except(requests.exceptions.ConnectionError,requests.exceptions.ReadTimeout):
+674time.sleep(1)
+675print(f"{i} ",end='',flush=True)
+676i+=1
+677
+678ifrisnotNone:
+679# If the answer is ok, process data and change state
+680ifr.ok:
+681print("Connected")
+682self.auth_data=r.json()
+683self.authorization={
+684"username":self.auth_data["user_info"]["username"],
+685"password":self.auth_data["user_info"]["password"]
+686}
+687# Account expiration date
+688self.account_expiration=timedelta(
+689seconds=(
+690int(self.auth_data["user_info"]["exp_date"])-datetime.now().timestamp()
+691)
+692)
+693# Mark connection authorized
+694self.state["authenticated"]=True
+695# Construct the base url for all requests
+696self.base_url=f"{self.server}/player_api.php?username={self.username}&password={self.password}"
+697# If there is a secure server connection, construct the base url SSL for all requests
+698if"https_port"inself.auth_data["server_info"]:
+699self.base_url_ssl=f"https://{self.auth_data['server_info']['url']}:{self.auth_data['server_info']['https_port']}" \
+700f"/player_api.php?username={self.username}&password={self.password}"
+701print(f"Account expires in {str(self.account_expiration)}")
+702else:
+703print(f"Provider `{self.name}` could not be loaded. Reason: `{r.status_code}{r.reason}`")
+704else:
+705print(f"\n{self.name}: Provider refused the connection")
@@ -4443,221 +4676,230 @@
-
741defload_iptv(self)->bool:
-742"""Load XTream IPTV
-743
-744 - Add all Live TV to XTream.channels
-745 - Add all VOD to XTream.movies
-746 - Add all Series to XTream.series
-747 Series contains Seasons and Episodes. Those are not automatically
-748 retrieved from the server to reduce the loading time.
-749 - Add all groups to XTream.groups
-750 Groups are for all three channel types, Live TV, VOD, and Series
-751
-752 Returns:
-753 bool: True if successfull, False if error
-754 """
-755# If pyxtream has not authenticated the connection, return empty
-756ifself.state["authenticated"]isFalse:
-757print("Warning, cannot load steams since authorization failed")
-758returnFalse
-759
-760# If pyxtream has already loaded the data, skip and return success
-761ifself.state["loaded"]isTrue:
-762print("Warning, data has already been loaded.")
-763returnTrue
-764
-765forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
-766## Get GROUPS
-767
-768# Try loading local file
-769dt=0
-770start=timer()
-771all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
-772# If file empty or does not exists, download it from remote
-773ifall_catisNone:
-774# Load all Groups and save file locally
-775all_cat=self._load_categories_from_provider(loading_stream_type)
-776ifall_catisnotNone:
-777self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
-778dt=timer()-start
-779
-780# If we got the GROUPS data, show the statistics and load GROUPS
-781ifall_catisnotNone:
-782print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
-783## Add GROUPS to dictionaries
+
766defload_iptv(self)->bool:
+767"""Load XTream IPTV
+768
+769 - Add all Live TV to XTream.channels
+770 - Add all VOD to XTream.movies
+771 - Add all Series to XTream.series
+772 Series contains Seasons and Episodes. Those are not automatically
+773 retrieved from the server to reduce the loading time.
+774 - Add all groups to XTream.groups
+775 Groups are for all three channel types, Live TV, VOD, and Series
+776
+777 Returns:
+778 bool: True if successful, False if error
+779 """
+780# If pyxtream has not authenticated the connection, return empty
+781ifself.state["authenticated"]isFalse:
+782print("Warning, cannot load steams since authorization failed")
+783returnFalse784
-785# Add the catch-all-errors group
-786ifloading_stream_type==self.live_type:
-787self.groups.append(self.live_catch_all_group)
-788elifloading_stream_type==self.vod_type:
-789self.groups.append(self.vod_catch_all_group)
-790elifloading_stream_type==self.series_type:
-791self.groups.append(self.series_catch_all_group)
-792
-793forcat_objinall_cat:
-794ifschemaValidator(cat_obj,SchemaType.GROUP):
-795# Create Group (Category)
-796new_group=Group(cat_obj,loading_stream_type)
-797# Add to xtream class
-798self.groups.append(new_group)
-799else:
-800# Save what did not pass schema validation
-801print(cat_obj)
-802
-803# Sort Categories
-804self.groups.sort(key=lambdax:x.name)
-805else:
-806print(f" - Could not load {loading_stream_type} Groups")
-807break
-808
-809## Get Streams
-810
-811# Try loading local file
-812dt=0
-813start=timer()
-814all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
-815# If file empty or does not exists, download it from remote
-816ifall_streamsisNone:
-817# Load all Streams and save file locally
-818all_streams=self._load_streams_from_provider(loading_stream_type)
-819self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
-820dt=timer()-start
-821
-822# If we got the STREAMS data, show the statistics and load Streams
-823ifall_streamsisnotNone:
-824print(f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams in {dt:.3f} seconds")
-825## Add Streams to dictionaries
+785# If pyxtream has already loaded the data, skip and return success
+786ifself.state["loaded"]isTrue:
+787print("Warning, data has already been loaded.")
+788returnTrue
+789
+790# Delete skipped channels from cache
+791full_filename=osp.join(self.cache_path,"skipped_streams.json")
+792try:
+793f=open(full_filename,mode="r+",encoding="utf-8")
+794f.truncate(0)
+795f.close()
+796exceptFileNotFoundError:
+797pass
+798
+799forloading_stream_typein(self.live_type,self.vod_type,self.series_type):
+800# Get GROUPS
+801
+802# Try loading local file
+803dt=0
+804start=timer()
+805all_cat=self._load_from_file(f"all_groups_{loading_stream_type}.json")
+806# If file empty or does not exists, download it from remote
+807ifall_catisNone:
+808# Load all Groups and save file locally
+809all_cat=self._load_categories_from_provider(loading_stream_type)
+810ifall_catisnotNone:
+811self._save_to_file(all_cat,f"all_groups_{loading_stream_type}.json")
+812dt=timer()-start
+813
+814# If we got the GROUPS data, show the statistics and load GROUPS
+815ifall_catisnotNone:
+816print(f"{self.name}: Loaded {len(all_cat)}{loading_stream_type} Groups in {dt:.3f} seconds")
+817# Add GROUPS to dictionaries
+818
+819# Add the catch-all-errors group
+820ifloading_stream_type==self.live_type:
+821self.groups.append(self.live_catch_all_group)
+822elifloading_stream_type==self.vod_type:
+823self.groups.append(self.vod_catch_all_group)
+824elifloading_stream_type==self.series_type:
+825self.groups.append(self.series_catch_all_group)826
-827skipped_adult_content=0
-828skipped_no_name_content=0
-829
-830number_of_streams=len(all_streams)
-831current_stream_number=0
-832# Calculate 1% of total number of streams
-833# This is used to slow down the progress bar
-834one_percent_number_of_streams=number_of_streams/100
-835start=timer()
-836forstream_channelinall_streams:
-837skip_stream=False
-838current_stream_number+=1
-839
-840# Show download progress every 1% of total number of streams
-841ifcurrent_stream_number<one_percent_number_of_streams:
-842progress(
-843current_stream_number,
-844number_of_streams,
-845f"Processing {loading_stream_type} Streams"
-846)
-847one_percent_number_of_streams*=2
-848
-849# Validate JSON scheme
-850ifself.validate_json:
-851ifloading_stream_type==self.series_type:
-852ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
-853print(stream_channel)
-854elifloading_stream_type==self.live_type:
-855ifnotschemaValidator(stream_channel,SchemaType.LIVE):
-856print(stream_channel)
-857else:
-858# vod_type
-859ifnotschemaValidator(stream_channel,SchemaType.VOD):
-860print(stream_channel)
-861
-862# Skip if the name of the stream is empty
-863ifstream_channel["name"]=="":
-864skip_stream=True
-865skipped_no_name_content=skipped_no_name_content+1
-866self._save_to_file_skipped_streams(stream_channel)
-867
-868# Skip if the user chose to hide adult streams
-869ifself.hide_adult_contentandloading_stream_type==self.live_type:
-870if"is_adult"instream_channel:
-871ifstream_channel["is_adult"]=="1":
-872skip_stream=True
-873skipped_adult_content=skipped_adult_content+1
-874self._save_to_file_skipped_streams(stream_channel)
-875
-876ifnotskip_stream:
-877# Some channels have no group,
-878# so let's add them to the catch all group
-879ifstream_channel["category_id"]=="":
-880stream_channel["category_id"]="9999"
-881elifstream_channel["category_id"]!="1":
-882pass
-883
-884# Find the first occurence of the group that the
-885# Channel or Stream is pointing to
-886the_group=next(
-887(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
-888None
-889)
-890
-891# Set group title
-892ifthe_groupisnotNone:
-893group_title=the_group.name
-894else:
-895ifloading_stream_type==self.live_type:
-896group_title=self.live_catch_all_group.name
-897the_group=self.live_catch_all_group
-898elifloading_stream_type==self.vod_type:
-899group_title=self.vod_catch_all_group.name
-900the_group=self.vod_catch_all_group
-901elifloading_stream_type==self.series_type:
-902group_title=self.series_catch_all_group.name
-903the_group=self.series_catch_all_group
-904
-905
-906ifloading_stream_type==self.series_type:
-907# Load all Series
-908new_series=Serie(self,stream_channel)
-909# To get all the Episodes for every Season of each
-910# Series is very time consuming, we will only
-911# populate the Series once the user click on the
-912# Series, the Seasons and Episodes will be loaded
-913# using x.getSeriesInfoByID() function
-914
-915else:
-916new_channel=Channel(
-917self,
-918group_title,
-919stream_channel
-920)
-921
-922ifnew_channel.group_id=="9999":
-923print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+827forcat_objinall_cat:
+828ifschemaValidator(cat_obj,SchemaType.GROUP):
+829# Create Group (Category)
+830new_group=Group(cat_obj,loading_stream_type)
+831# Add to xtream class
+832self.groups.append(new_group)
+833else:
+834# Save what did not pass schema validation
+835print(cat_obj)
+836
+837# Sort Categories
+838self.groups.sort(key=lambdax:x.name)
+839else:
+840print(f" - Could not load {loading_stream_type} Groups")
+841break
+842
+843# Get Streams
+844
+845# Try loading local file
+846dt=0
+847start=timer()
+848all_streams=self._load_from_file(f"all_stream_{loading_stream_type}.json")
+849# If file empty or does not exists, download it from remote
+850ifall_streamsisNone:
+851# Load all Streams and save file locally
+852all_streams=self._load_streams_from_provider(loading_stream_type)
+853self._save_to_file(all_streams,f"all_stream_{loading_stream_type}.json")
+854dt=timer()-start
+855
+856# If we got the STREAMS data, show the statistics and load Streams
+857ifall_streamsisnotNone:
+858print(f"{self.name}: Loaded {len(all_streams)}{loading_stream_type} Streams in {dt:.3f} seconds")
+859# Add Streams to dictionaries
+860
+861skipped_adult_content=0
+862skipped_no_name_content=0
+863
+864number_of_streams=len(all_streams)
+865current_stream_number=0
+866# Calculate 1% of total number of streams
+867# This is used to slow down the progress bar
+868one_percent_number_of_streams=number_of_streams/100
+869start=timer()
+870forstream_channelinall_streams:
+871skip_stream=False
+872current_stream_number+=1
+873
+874# Show download progress every 1% of total number of streams
+875ifcurrent_stream_number<one_percent_number_of_streams:
+876progress(
+877current_stream_number,
+878number_of_streams,
+879f"Processing {loading_stream_type} Streams"
+880)
+881one_percent_number_of_streams*=2
+882
+883# Validate JSON scheme
+884ifself.validate_json:
+885ifloading_stream_type==self.series_type:
+886ifnotschemaValidator(stream_channel,SchemaType.SERIES_INFO):
+887print(stream_channel)
+888elifloading_stream_type==self.live_type:
+889ifnotschemaValidator(stream_channel,SchemaType.LIVE):
+890print(stream_channel)
+891else:
+892# vod_type
+893ifnotschemaValidator(stream_channel,SchemaType.VOD):
+894print(stream_channel)
+895
+896# Skip if the name of the stream is empty
+897ifstream_channel["name"]=="":
+898skip_stream=True
+899skipped_no_name_content=skipped_no_name_content+1
+900self._save_to_file_skipped_streams(stream_channel)
+901
+902# Skip if the user chose to hide adult streams
+903ifself.hide_adult_contentandloading_stream_type==self.live_type:
+904if"is_adult"instream_channel:
+905ifstream_channel["is_adult"]=="1":
+906skip_stream=True
+907skipped_adult_content=skipped_adult_content+1
+908self._save_to_file_skipped_streams(stream_channel)
+909
+910ifnotskip_stream:
+911# Some channels have no group,
+912# so let's add them to the catch all group
+913ifnotstream_channel["category_id"]:
+914stream_channel["category_id"]="9999"
+915elifstream_channel["category_id"]!="1":
+916pass
+917
+918# Find the first occurrence of the group that the
+919# Channel or Stream is pointing to
+920the_group=next(
+921(xforxinself.groupsifx.group_id==int(stream_channel["category_id"])),
+922None
+923)924
-925# Save the new channel to the local list of channels
-926ifloading_stream_type==self.live_type:
-927self.channels.append(new_channel)
-928elifloading_stream_type==self.vod_type:
-929self.movies.append(new_channel)
-930ifnew_channel.age_days_from_added<31:
-931self.movies_30days.append(new_channel)
-932ifnew_channel.age_days_from_added<7:
-933self.movies_7days.append(new_channel)
-934else:
-935self.series.append(new_series)
-936
-937# Add stream to the specific Group
-938ifthe_groupisnotNone:
-939ifloading_stream_type!=self.series_type:
-940the_group.channels.append(new_channel)
-941else:
-942the_group.series.append(new_series)
-943else:
-944print(f" - Group not found `{stream_channel['name']}`")
-945print("\n")
-946# Print information of which streams have been skipped
-947ifself.hide_adult_content:
-948print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
-949ifskipped_no_name_content>0:
-950print(f" - Skipped {skipped_no_name_content} "
-951"unprintable {loading_stream_type} streams")
-952else:
-953print(f" - Could not load {loading_stream_type} Streams")
+925# Set group title
+926ifthe_groupisnotNone:
+927group_title=the_group.name
+928else:
+929ifloading_stream_type==self.live_type:
+930group_title=self.live_catch_all_group.name
+931the_group=self.live_catch_all_group
+932elifloading_stream_type==self.vod_type:
+933group_title=self.vod_catch_all_group.name
+934the_group=self.vod_catch_all_group
+935elifloading_stream_type==self.series_type:
+936group_title=self.series_catch_all_group.name
+937the_group=self.series_catch_all_group
+938
+939ifloading_stream_type==self.series_type:
+940# Load all Series
+941new_series=Serie(self,stream_channel)
+942# To get all the Episodes for every Season of each
+943# Series is very time consuming, we will only
+944# populate the Series once the user click on the
+945# Series, the Seasons and Episodes will be loaded
+946# using x.getSeriesInfoByID() function
+947
+948else:
+949new_channel=Channel(
+950self,
+951group_title,
+952stream_channel
+953)954
-955self.state["loaded"]=True
+955ifnew_channel.group_id=="9999":
+956print(f" - xEverythingElse Channel -> {new_channel.name} - {new_channel.stream_type}")
+957
+958# Save the new channel to the local list of channels
+959ifloading_stream_type==self.live_type:
+960self.channels.append(new_channel)
+961elifloading_stream_type==self.vod_type:
+962self.movies.append(new_channel)
+963ifnew_channel.age_days_from_added<31:
+964self.movies_30days.append(new_channel)
+965ifnew_channel.age_days_from_added<7:
+966self.movies_7days.append(new_channel)
+967else:
+968self.series.append(new_series)
+969
+970# Add stream to the specific Group
+971ifthe_groupisnotNone:
+972ifloading_stream_type!=self.series_type:
+973the_group.channels.append(new_channel)
+974else:
+975the_group.series.append(new_series)
+976else:
+977print(f" - Group not found `{stream_channel['name']}`")
+978print("\n")
+979# Print information of which streams have been skipped
+980ifself.hide_adult_content:
+981print(f" - Skipped {skipped_adult_content} adult {loading_stream_type} streams")
+982ifskipped_no_name_content>0:
+983print(f" - Skipped {skipped_no_name_content} "
+984"unprintable {loading_stream_type} streams")
+985else:
+986print(f" - Could not load {loading_stream_type} Streams")
+987
+988self.state["loaded"]=True
+989returnTrue
@@ -4674,7 +4916,7 @@
Returns:
- bool: True if successfull, False if error
+ bool: True if successful, False if error
@@ -4690,30 +4932,31 @@
-
972defget_series_info_by_id(self,get_series:dict):
-973"""Get Seasons and Episodes for a Series
-974
-975 Args:
-976 get_series (dict): Series dictionary
-977 """
-978
-979series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
-980
-981ifseries_seasons["seasons"]isNone:
-982series_seasons["seasons"]=[{"name":"Season 1","cover":series_seasons["info"]["cover"]}]
-983
-984forseries_infoinseries_seasons["seasons"]:
-985season_name=series_info["name"]
-986season_key=series_info['season_number']
-987season=Season(season_name)
-988get_series.seasons[season_name]=season
-989if"episodes"inseries_seasons.keys():
-990forseries_seasoninseries_seasons["episodes"].keys():
-991forepisode_infoinseries_seasons["episodes"][str(series_season)]:
-992new_episode_channel=Episode(
-993self,series_info,"Testing",episode_info
-994)
-995season.episodes[episode_info["title"]]=new_episode_channel
+
1007defget_series_info_by_id(self,get_series:dict):
+1008"""Get Seasons and Episodes for a Series
+1009
+1010 Args:
+1011 get_series (dict): Series dictionary
+1012 """
+1013
+1014series_seasons=self._load_series_info_by_id_from_provider(get_series.series_id)
+1015
+1016ifseries_seasons["seasons"]isNone:
+1017series_seasons["seasons"]=[
+1018{"name":"Season 1","cover":series_seasons["info"]["cover"]}
+1019]
+1020
+1021forseries_infoinseries_seasons["seasons"]:
+1022season_name=series_info["name"]
+1023season=Season(season_name)
+1024get_series.seasons[season_name]=season
+1025if"episodes"inseries_seasons.keys():
+1026forseries_seasoninseries_seasons["episodes"].keys():
+1027forepisode_infoinseries_seasons["episodes"][str(series_season)]:
+1028new_episode_channel=Episode(
+1029self,series_info,"Testing",episode_info
+1030)
+1031season.episodes[episode_info["title"]]=new_episode_channel
Args:\n provider_name (str): Name of the IPTV provider\n provider_username (str): User name of the IPTV provider\n provider_password (str): Password of the IPTV provider\n provider_url (str): URL of the IPTV provider\n headers (dict): Requests Headers\n hide_adult_content(bool, optional): When True hide stream that are marked for adult\n cache_path (str, optional): Location where to save loaded files.\n Defaults to empty string.\n reload_time_sec (int, optional): Number of seconds before automatic reloading\n (-1 to turn it OFF)\n debug_flask (bool, optional): Enable the debug mode in Flask\n validate_json (bool, optional): Check Xtream API provided JSON for validity
\n\n
Returns: XTream Class Instance
\n\n
\n
Note 1: If it fails to authorize with provided username and password,\nauth_data will be an empty dictionary.
\n
Note 2: The JSON validation option will take considerable amount of time and it should be \nused only as a debug tool. The Xtream API JSON from the provider passes through a\nschema that represent the best available understanding of how the Xtream API \nworks.
Args:\n keyword (str): Keyword to search for. Supports REGEX\n ignore_case (bool, optional): True to ignore case during search. Defaults to \"True\".\n return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to \"LIST\".\n stream_type (list, optional): Search within specific stream type.
\n\n
Returns:\n list: List with all the results, it could be empty.
Add all Series to XTream.series\nSeries contains Seasons and Episodes. Those are not automatically\nretrieved from the server to reduce the loading time.
\n
Add all groups to XTream.groups\nGroups are for all three channel types, Live TV, VOD, and Series
\n
\n\n
Returns:\n bool: True if successfull, False if error
This class can be safely subclassed in a limited fashion. There are two ways\nto specify the activity: by passing a callable object to the constructor, or\nby overriding the run() method in a subclass.
This constructor should always be called with keyword arguments. Arguments are:
\n\n
group should be None; reserved for future extension when a ThreadGroup\nclass is implemented.
\n\n
target is the callable object to be invoked by the run()\nmethod. Defaults to None, meaning nothing is called.
\n\n
name is the thread name. By default, a unique name is constructed of\nthe form \"Thread-N\" where N is a small decimal number.
\n\n
args is a list or tuple of arguments for the target invocation. Defaults to ().
\n\n
kwargs is a dictionary of keyword arguments for the target\ninvocation. Defaults to {}.
\n\n
If a subclass overrides the constructor, it must make sure to invoke\nthe base class constructor (Thread.__init__()) before doing anything\nelse to the thread.
A boolean value indicating whether this thread is a daemon thread.
\n\n
This must be set before start() is called, otherwise RuntimeError is\nraised. Its initial value is inherited from the creating thread; the\nmain thread is not a daemon thread and therefore all threads created in\nthe main thread default to daemon = False.
\n\n
The entire Python program exits when only daemon threads are left.
You may override this method in a subclass. The standard run() method\ninvokes the callable object passed to the object's constructor as the\ntarget argument, if any, with sequential and keyword arguments taken\nfrom the args and kwargs arguments, respectively.
Args:\n provider_name (str): Name of the IPTV provider\n provider_username (str): User name of the IPTV provider\n provider_password (str): Password of the IPTV provider\n provider_url (str): URL of the IPTV provider\n headers (dict): Requests Headers\n hide_adult_content(bool, optional): When True hide stream that are marked for adult\n cache_path (str, optional): Location where to save loaded files.\n Defaults to empty string.\n reload_time_sec (int, optional): Number of seconds before automatic reloading\n (-1 to turn it OFF)\n validate_json (bool, optional): Check Xtream API provided JSON for validity\n enable_flask (bool, optional): Enable Flask\n debug_flask (bool, optional): Enable the debug mode in Flask
\n\n
Returns: XTream Class Instance
\n\n
\n
Note 1: If it fails to authorize with provided username and password,\nauth_data will be an empty dictionary.
\n
Note 2: The JSON validation option will take considerable amount of time and it should be\nused only as a debug tool. The Xtream API JSON from the provider passes through a\nschema that represent the best available understanding of how the Xtream API\nworks.
Args:\n keyword (str): Keyword to search for. Supports REGEX\n ignore_case (bool, optional): True to ignore case during search. Defaults to \"True\".\n return_type (str, optional): Output format, 'LIST' or 'JSON'. Defaults to \"LIST\".\n stream_type (list, optional): Search within specific stream type.\n added_after (datetime, optional): Search for items that have been added after a certain date.
\n\n
Returns:\n list: List with all the results, it could be empty.
Add all Series to XTream.series\nSeries contains Seasons and Episodes. Those are not automatically\nretrieved from the server to reduce the loading time.
\n
Add all groups to XTream.groups\nGroups are for all three channel types, Live TV, VOD, and Series
\n
\n\n
Returns:\n bool: True if successful, False if error
This class can be safely subclassed in a limited fashion. There are two ways\nto specify the activity: by passing a callable object to the constructor, or\nby overriding the run() method in a subclass.
This constructor should always be called with keyword arguments. Arguments are:
\n\n
group should be None; reserved for future extension when a ThreadGroup\nclass is implemented.
\n\n
target is the callable object to be invoked by the run()\nmethod. Defaults to None, meaning nothing is called.
\n\n
name is the thread name. By default, a unique name is constructed of\nthe form \"Thread-N\" where N is a small decimal number.
\n\n
args is a list or tuple of arguments for the target invocation. Defaults to ().
\n\n
kwargs is a dictionary of keyword arguments for the target\ninvocation. Defaults to {}.
\n\n
If a subclass overrides the constructor, it must make sure to invoke\nthe base class constructor (Thread.__init__()) before doing anything\nelse to the thread.
A boolean value indicating whether this thread is a daemon thread.
\n\n
This must be set before start() is called, otherwise RuntimeError is\nraised. Its initial value is inherited from the creating thread; the\nmain thread is not a daemon thread and therefore all threads created in\nthe main thread default to daemon = False.
\n\n
The entire Python program exits when only daemon threads are left.
You may override this method in a subclass. The standard run() method\ninvokes the callable object passed to the object's constructor as the\ntarget argument, if any, with sequential and keyword arguments taken\nfrom the args and kwargs arguments, respectively.