|
""" |
|
MIT License |
|
|
|
Copyright (C) 2023 ROCKY4546 |
|
https://github.com/rocky4546 |
|
|
|
This file is part of Cabernet |
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software |
|
and associated documentation files (the "Software"), to deal in the Software without restriction, |
|
including without limitation the rights to use, copy, modify, merge, publish, distribute, |
|
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software |
|
is furnished to do so, subject to the following conditions: |
|
|
|
The above copyright notice and this permission notice shall be included in all copies or |
|
substantial portions of the Software. |
|
""" |
|
|
|
import collections |
|
import datetime |
|
import json |
|
import logging |
|
import io |
|
import re |
|
import threading |
|
import time |
|
|
|
import lib.m3u8 as m3u8 |
|
import lib.config.config_callbacks as config_callbacks |
|
import lib.common.utils as utils |
|
import lib.image_size.get_image_size as get_image_size |
|
from lib.db.db_channels import DBChannels |
|
from lib.common.decorators import handle_url_except |
|
from lib.common.decorators import handle_json_except |
|
|
|
class PluginChannels: |
|
|
|
def __init__(self, _instance_obj): |
|
self.logger = logging.getLogger(__name__) |
|
self.instance_obj = _instance_obj |
|
self.config_obj = self.instance_obj.config_obj |
|
self.plugin_obj = _instance_obj.plugin_obj |
|
self.instance_key = _instance_obj.instance_key |
|
self.db = DBChannels(self.config_obj.data) |
|
self.config_section = self.instance_obj.config_section |
|
|
|
self.ch_num_enum = self.config_obj.data[self.config_section].get('channel-start_ch_num') |
|
if self.ch_num_enum is None or self.ch_num_enum < 0: |
|
self.ch_num_enum = 0 |
|
|
|
def terminate(self): |
|
""" |
|
Removes all has a object from the object and calls any subclasses to also terminate |
|
Not calling inherited class at this time |
|
""" |
|
self.logger = None |
|
self.instance_obj = None |
|
self.config_obj = None |
|
self.plugin_obj = None |
|
self.instance_key = None |
|
self.db = None |
|
self.config_section = None |
|
self.ch_num_enum = None |
|
|
|
def set_channel_num(self, _number): |
|
""" |
|
if _number is None then will set the channel number based |
|
on the enum counter |
|
""" |
|
if _number is None: |
|
ch_number = self.ch_num_enum |
|
self.ch_num_enum += 1 |
|
return ch_number |
|
else: |
|
return _number |
|
|
|
def get_channels(self): |
|
""" |
|
Interface method to override |
|
""" |
|
pass |
|
|
|
@handle_url_except() |
|
@handle_json_except |
|
def get_uri_json_data(self, _uri, _retries): |
|
header = { |
|
'Content-Type': 'application/json', |
|
'User-agent': utils.DEFAULT_USER_AGENT} |
|
resp = self.plugin_obj.http_session.get(_uri, headers=header, timeout=8) |
|
x = resp.json() |
|
resp.raise_for_status() |
|
return x |
|
|
|
@handle_url_except() |
|
def get_uri_data(self, _uri, _retries, _header=None, _data=None, _cookies=None): |
|
if _header is None: |
|
header = { |
|
'User-agent': utils.DEFAULT_USER_AGENT} |
|
else: |
|
header = _header |
|
if _data: |
|
resp = self.plugin_obj.http_session.post(_uri, headers=header, data=_data, timeout=18, verify=False, cookies=_cookies) |
|
else: |
|
resp = self.plugin_obj.http_session.get(_uri, headers=header, timeout=18, verify=False, cookies=_cookies) |
|
x = resp.content |
|
return x |
|
|
|
@handle_url_except() |
|
def get_m3u8_data(self, _uri, _retries, _header=None): |
|
if _header is None: |
|
return m3u8.load(_uri, |
|
headers={'User-agent': utils.DEFAULT_USER_AGENT}, |
|
http_session=self.plugin_obj.http_session) |
|
else: |
|
return m3u8.load(_uri, |
|
headers=_header, |
|
http_session=self.plugin_obj.http_session) |
|
|
|
def refresh_channels(self, force=False): |
|
self.ch_num_enum = self.config_obj.data[self.config_section].get('channel-start_ch_num') |
|
if self.ch_num_enum is None or self.ch_num_enum < 0: |
|
self.ch_num_enum = 0 |
|
last_update = self.db.get_status(self.plugin_obj.name, self.instance_key) |
|
update_needed = False |
|
if not last_update: |
|
update_needed = True |
|
else: |
|
delta = datetime.datetime.now() - last_update |
|
if delta.total_seconds() / 3600 >= \ |
|
self.config_obj.data[self.config_section]['channel-update_timeout']: |
|
update_needed = True |
|
if update_needed or force: |
|
i = 0 |
|
ch_dict = self.get_channels() |
|
while ch_dict is None and i < 2: |
|
i += 1 |
|
time.sleep(0.5) |
|
ch_dict = self.get_channels() |
|
if ch_dict is None: |
|
self.logger.warning( |
|
'Unable to retrieve channel data from {}:{}, aborting refresh' |
|
.format(self.plugin_obj.name, self.instance_key)) |
|
return False |
|
if 'channel-import_groups' in self.config_obj.data[self.config_section]: |
|
self.db.save_channel_list( |
|
self.plugin_obj.name, self.instance_key, ch_dict, |
|
self.config_obj.data[self.config_section]['channel-import_groups']) |
|
else: |
|
self.db.save_channel_list(self.plugin_obj.name, self.instance_key, ch_dict) |
|
if self.config_obj.data[self.config_section].get('channel-start_ch_num') > -1: |
|
config_callbacks.update_channel_num(self.config_obj, self.config_section, 'channel-start_ch_num') |
|
self.logger.debug( |
|
'{}:{} Channel update complete' |
|
.format(self.plugin_obj.name, self.instance_key)) |
|
else: |
|
self.logger.debug( |
|
'Channel data still new for {} {}, not refreshing' |
|
.format(self.plugin_obj.name, self.instance_key)) |
|
return False |
|
|
|
return True |
|
|
|
def clean_group_name(self, group_name): |
|
return re.sub('[ +&*%$#@!:;,<>?]', '', group_name) |
|
|
|
@handle_url_except() |
|
def get_thumbnail_size(self, _thumbnail, _retries, _ch_uid, ): |
|
thumbnail_size = (0, 0) |
|
if _thumbnail is None or _thumbnail == '': |
|
return thumbnail_size |
|
|
|
if _ch_uid is not None: |
|
ch_row = self.db.get_channel(_ch_uid, self.plugin_obj.name, self.instance_key) |
|
if ch_row is not None: |
|
if ch_row['json']['thumbnail'] == _thumbnail: |
|
return ch_row['json']['thumbnail_size'] |
|
|
|
h = {'User-Agent': utils.DEFAULT_USER_AGENT, |
|
'Accept': '*/*', |
|
'Accept-Encoding': 'identity', |
|
'Connection': 'Keep-Alive' |
|
} |
|
resp = self.plugin_obj.http_session.get(_thumbnail, headers=h, timeout=8) |
|
resp.raise_for_status() |
|
img_blob = resp.content |
|
fp = io.BytesIO(img_blob) |
|
sz = len(img_blob) |
|
try: |
|
thumbnail_size = get_image_size.get_image_size_from_bytesio(fp, sz) |
|
except get_image_size.UnknownImageFormat as e: |
|
self.logger.warning('{}: Thumbnail unknown format. {}' |
|
.format(self.plugin_obj.name, str(e))) |
|
pass |
|
return thumbnail_size |
|
|
|
@handle_url_except |
|
def get_best_stream(self, _url, _retries, _channel_id, _referer=None): |
|
if self.config_obj.data[self.config_section]['player-stream_type'] == 'm3u8redirect': |
|
return _url |
|
|
|
|
|
self.logger.debug( |
|
'{}: Getting best video stream info for {} {}' |
|
.format(self.plugin_obj.name, _channel_id, _url)) |
|
best_stream = None |
|
if _referer: |
|
header = { |
|
'User-agent': utils.DEFAULT_USER_AGENT, |
|
'Referer': _referer} |
|
else: |
|
header = {'User-agent': utils.DEFAULT_USER_AGENT} |
|
|
|
ch_dict = self.db.get_channel(_channel_id, self.plugin_obj.name, self.instance_key) |
|
ch_json = ch_dict['json'] |
|
best_resolution = -1 |
|
video_url_m3u = m3u8.load( |
|
_url, headers=header, |
|
http_session=self.plugin_obj.http_session) |
|
|
|
if not video_url_m3u: |
|
self.logger.notice('{}:{} Unable to obtain m3u file, aborting stream {}' |
|
.format(self.plugin_obj.name, self.instance_key, _channel_id)) |
|
return |
|
self.logger.debug("Found " + str(len(video_url_m3u.playlists)) + " Playlists") |
|
|
|
if len(video_url_m3u.playlists) > 0: |
|
max_bitrate = self.config_obj.data[self.config_section]['player-stream_quality'] |
|
bitrate_list = {} |
|
for video_stream in video_url_m3u.playlists: |
|
bitrate_list[video_stream.stream_info.bandwidth] = video_stream |
|
bitrate_list = collections.OrderedDict(sorted(bitrate_list.items(), reverse=True)) |
|
|
|
if list(bitrate_list.keys())[0] > max_bitrate: |
|
is_set_by_bitrate = True |
|
else: |
|
is_set_by_bitrate = False |
|
for bps, seg in bitrate_list.items(): |
|
if bps < max_bitrate: |
|
best_stream = seg |
|
if seg.stream_info.resolution: |
|
best_resolution = seg.stream_info.resolution[1] |
|
break |
|
else: |
|
best_stream = seg |
|
if seg.stream_info.resolution: |
|
best_resolution = seg.stream_info.resolution[1] |
|
|
|
for video_stream in video_url_m3u.playlists: |
|
if best_stream is None: |
|
best_stream = video_stream |
|
if video_stream.stream_info.resolution: |
|
best_resolution = video_stream.stream_info.resolution[1] |
|
elif not video_stream.stream_info.resolution: |
|
|
|
continue |
|
elif ((video_stream.stream_info.resolution[0] > best_stream.stream_info.resolution[0]) and |
|
(video_stream.stream_info.resolution[1] > best_stream.stream_info.resolution[1]) and |
|
not is_set_by_bitrate): |
|
best_stream = video_stream |
|
best_resolution = video_stream.stream_info.resolution[1] |
|
elif ((video_stream.stream_info.resolution[0] == best_stream.stream_info.resolution[0]) and |
|
(video_stream.stream_info.resolution[1] == best_stream.stream_info.resolution[1]) and |
|
(video_stream.stream_info.bandwidth > best_stream.stream_info.bandwidth) and |
|
not is_set_by_bitrate): |
|
best_stream = video_stream |
|
best_resolution = video_stream.stream_info.resolution[1] |
|
|
|
json_needs_updating = False |
|
if best_stream is not None: |
|
|
|
|
|
if best_resolution >= 720 and ch_json['HD'] == 0: |
|
ch_json['HD'] = 1 |
|
json_needs_updating = True |
|
elif 0 < best_resolution < 720 and ch_json['HD'] == 1: |
|
ch_json['HD'] = 0 |
|
json_needs_updating = True |
|
elif best_stream.stream_info.bandwidth > 3000000 and ch_json['HD'] == 0: |
|
ch_json['HD'] = 1 |
|
json_needs_updating = True |
|
elif best_stream.stream_info.bandwidth <= 3000000 and ch_json['HD'] == 1: |
|
ch_json['HD'] = 0 |
|
json_needs_updating = True |
|
|
|
if best_stream.stream_info.resolution is None: |
|
self.logger.debug( |
|
'{} will use bandwidth at {} bps' |
|
.format(_channel_id, str(best_stream.stream_info.bandwidth))) |
|
else: |
|
self.logger.notice( |
|
self.plugin_obj.name + ': ' + _channel_id + " will use " + |
|
str(best_stream.stream_info.resolution[0]) + "x" + |
|
str(best_stream.stream_info.resolution[1]) + |
|
" resolution at " + str(best_stream.stream_info.bandwidth) + "bps") |
|
|
|
if json_needs_updating: |
|
self.db.update_channel_json(ch_json, self.plugin_obj.name, self.instance_key) |
|
return best_stream.absolute_uri |
|
else: |
|
self.logger.debug('{}: {} No variant streams found for this station. Assuming single stream only.' |
|
.format(self.plugin_obj.name, _channel_id)) |
|
return _url |
|
|
|
def check_logger_refresh(self): |
|
if not self.logger.isEnabledFor(40): |
|
self.logger = logging.getLogger(__name__ + str(threading.get_ident())) |
|
|