2018-03-13 00:33:39 +01:00
|
|
|
import logging
|
2023-12-10 20:42:34 +01:00
|
|
|
import operator
|
|
|
|
import re
|
2018-03-13 00:33:39 +01:00
|
|
|
from operator import itemgetter
|
2021-05-16 02:22:37 +02:00
|
|
|
from typing import List
|
2018-03-13 00:33:39 +01:00
|
|
|
|
|
|
|
from svtplay_dl import error
|
2019-08-25 00:40:39 +02:00
|
|
|
from svtplay_dl.utils.http import HTTP
|
2018-03-13 00:33:39 +01:00
|
|
|
|
|
|
|
|
|
|
|
# TODO: should be set as the default option in the argument parsing?
|
2021-05-09 01:16:16 +02:00
|
|
|
DEFAULT_PROTOCOL_PRIO = ["dash", "hls", "http"]
|
|
|
|
LIVE_PROTOCOL_PRIO = ["hls", "dash", "http"]
|
2020-07-28 21:30:24 +02:00
|
|
|
DEFAULT_FORMAT_PRIO = ["h264", "h264-51"]
|
2023-12-10 20:42:34 +01:00
|
|
|
OPERATORS = {
|
|
|
|
"<": operator.lt,
|
|
|
|
"<=": operator.le,
|
|
|
|
">": operator.gt,
|
|
|
|
">=": operator.ge,
|
|
|
|
}
|
2018-03-13 00:33:39 +01:00
|
|
|
|
2018-03-13 00:44:34 +01:00
|
|
|
|
2021-05-16 02:22:37 +02:00
|
|
|
def sort_quality(data) -> List:
|
2018-05-25 22:47:26 +02:00
|
|
|
data = sorted(data, key=lambda x: (x.bitrate, x.name), reverse=True)
|
2018-03-13 00:33:39 +01:00
|
|
|
datas = []
|
|
|
|
for i in data:
|
2021-05-16 02:22:37 +02:00
|
|
|
datas.append([i.bitrate, i.name, i.format, i.resolution, i.language, i.audio_role])
|
2018-03-13 00:33:39 +01:00
|
|
|
return datas
|
|
|
|
|
|
|
|
|
|
|
|
def list_quality(videos):
|
2021-05-16 12:14:19 +02:00
|
|
|
data = [["Quality:", "Method:", "Codec:", "Resolution:", "Language:", "Role:"]]
|
|
|
|
data.extend(sort_quality(videos))
|
|
|
|
for i in range(len(data)):
|
|
|
|
logging.info(
|
2021-05-16 21:00:57 +02:00
|
|
|
f"{str(data[i][0]):<10s} {data[i][1].upper():<8s} {data[i][2]:<8s} {data[i][3]:<12s} {data[i][4]:<20s} {data[i][5]:<20s}",
|
2021-05-16 12:14:19 +02:00
|
|
|
)
|
2018-03-13 00:33:39 +01:00
|
|
|
|
|
|
|
|
2021-05-16 02:22:37 +02:00
|
|
|
def protocol_prio(streams, priolist) -> List:
|
2018-03-13 00:33:39 +01:00
|
|
|
"""
|
|
|
|
Given a list of VideoRetriever objects and a prioritized list of
|
|
|
|
accepted protocols (as strings) (highest priority first), return
|
|
|
|
a list of VideoRetriever objects that are accepted, and sorted
|
|
|
|
by bitrate, and then protocol priority.
|
|
|
|
"""
|
|
|
|
# Map score's to the reverse of the list's index values
|
|
|
|
proto_score = dict(zip(priolist, range(len(priolist), 0, -1)))
|
|
|
|
logging.debug("Protocol priority scores (higher is better): %s", str(proto_score))
|
|
|
|
|
|
|
|
# Build a tuple (bitrate, proto_score, stream), and use it
|
|
|
|
# for sorting.
|
2019-09-06 22:49:49 +02:00
|
|
|
prioritized = [(s.bitrate, proto_score[s.name], s) for s in streams if s.name in proto_score]
|
2018-03-13 00:33:39 +01:00
|
|
|
return [x[2] for x in sorted(prioritized, key=itemgetter(0, 1), reverse=True)]
|
|
|
|
|
|
|
|
|
2021-05-16 02:22:37 +02:00
|
|
|
def format_prio(streams, priolist) -> List:
|
2020-07-28 21:30:24 +02:00
|
|
|
logging.debug("Format priority: %s", str(priolist))
|
|
|
|
prioritized = [s for s in streams if s.format in priolist]
|
|
|
|
return prioritized
|
|
|
|
|
|
|
|
|
2021-05-16 02:22:37 +02:00
|
|
|
def language_prio(config, streams) -> List:
|
|
|
|
if config.get("audio_language"):
|
|
|
|
language = config.get("audio_language")
|
|
|
|
else:
|
|
|
|
return streams
|
|
|
|
prioritized = [s for s in streams if s.language == language]
|
|
|
|
return prioritized
|
|
|
|
|
|
|
|
|
|
|
|
def audio_role(config, streams) -> List:
|
|
|
|
if config.get("audio_role"):
|
|
|
|
role = config.get("audio_role")
|
|
|
|
elif config.get("audio_role") is None and config.get("audio_language"):
|
|
|
|
return streams
|
|
|
|
else:
|
|
|
|
role = "main"
|
|
|
|
|
|
|
|
prioritized = [s for s in streams if s.audio_role == role]
|
|
|
|
return prioritized
|
|
|
|
|
|
|
|
|
2021-05-22 22:57:43 +02:00
|
|
|
def subtitle_filter(subtitles) -> List:
|
|
|
|
languages = []
|
|
|
|
subs = []
|
2021-06-13 01:20:39 +02:00
|
|
|
if not subtitles:
|
|
|
|
return subs
|
2021-05-22 22:57:43 +02:00
|
|
|
preferred = subtitles[0].config.get("subtitle_preferred")
|
|
|
|
all_subs = subtitles[0].config.get("get_all_subtitles")
|
|
|
|
|
|
|
|
for sub in subtitles:
|
|
|
|
if sub.subfix not in languages:
|
2021-05-24 20:50:38 +02:00
|
|
|
if all_subs:
|
2023-11-28 23:59:57 +01:00
|
|
|
if sub.subfix is None:
|
|
|
|
continue
|
2021-05-22 22:57:43 +02:00
|
|
|
subs.append(sub)
|
|
|
|
languages.append(sub.subfix)
|
|
|
|
else:
|
2021-06-02 20:41:13 +02:00
|
|
|
if preferred is None:
|
|
|
|
subs.append(sub)
|
|
|
|
languages.append(sub.subfix)
|
|
|
|
if preferred and sub.subfix == preferred:
|
2021-05-24 20:50:38 +02:00
|
|
|
subs.append(sub)
|
|
|
|
languages.append(sub.subfix)
|
2021-05-22 22:57:43 +02:00
|
|
|
return subs
|
|
|
|
|
|
|
|
|
|
|
|
def subtitle_decider(stream, subtitles):
|
2023-06-02 20:26:32 +02:00
|
|
|
if subtitles and (stream.config.get("merge_subtitle") or stream.config.get("subtitle") or stream.config.get("get_all_subtitles")):
|
2021-05-22 22:57:43 +02:00
|
|
|
subtitles = subtitle_filter(subtitles)
|
|
|
|
if stream.config.get("get_all_subtitles"):
|
|
|
|
for sub in subtitles:
|
2023-11-28 23:59:57 +01:00
|
|
|
if sub.subfix:
|
|
|
|
if stream.config.get("get_url"):
|
|
|
|
print(sub.url)
|
|
|
|
else:
|
|
|
|
sub.download()
|
2021-05-22 22:57:43 +02:00
|
|
|
else:
|
|
|
|
if stream.config.get("get_url"):
|
|
|
|
print(subtitles[0].url)
|
|
|
|
else:
|
|
|
|
subtitles[0].download()
|
|
|
|
elif stream.config.get("merge_subtitle"):
|
|
|
|
stream.config.set("merge_subtitle", False)
|
|
|
|
|
|
|
|
|
2022-05-13 08:36:34 +02:00
|
|
|
def resolution(streams, resolutions: List) -> List:
|
|
|
|
videos = []
|
|
|
|
for stream in streams:
|
|
|
|
for resolution in resolutions:
|
2023-12-10 20:42:34 +01:00
|
|
|
match = re.match(r"(?P<op><=|>=|<|>)?(?P<res>[\d+]+)", resolution)
|
|
|
|
op, res = match.group("op", "res")
|
|
|
|
if op:
|
|
|
|
op = OPERATORS.get(op, operator.eq)
|
|
|
|
if op(int(stream.resolution.split("x")[1]), int(res)):
|
|
|
|
videos.append(stream)
|
|
|
|
else:
|
|
|
|
if stream.resolution.find("x") > 0 and stream.resolution.split("x")[1] == resolution:
|
|
|
|
videos.append(stream)
|
2022-05-13 08:36:34 +02:00
|
|
|
return videos
|
|
|
|
|
|
|
|
|
2018-05-08 22:46:11 +02:00
|
|
|
def select_quality(config, streams):
|
2018-03-13 00:33:39 +01:00
|
|
|
high = 0
|
2018-05-08 22:46:11 +02:00
|
|
|
if isinstance(config.get("quality"), str):
|
2018-03-13 00:33:39 +01:00
|
|
|
try:
|
2018-05-08 22:46:11 +02:00
|
|
|
quality = int(config.get("quality").split("-")[0])
|
|
|
|
if len(config.get("quality").split("-")) > 1:
|
|
|
|
high = int(config.get("quality").split("-")[1])
|
2018-03-13 00:33:39 +01:00
|
|
|
except ValueError:
|
2019-09-06 22:49:49 +02:00
|
|
|
raise error.UIException("Requested quality is invalid. use a number or range lowerNumber-higherNumber")
|
2018-03-13 00:33:39 +01:00
|
|
|
else:
|
2018-05-08 22:46:11 +02:00
|
|
|
quality = config.get("quality")
|
2018-03-13 00:33:39 +01:00
|
|
|
try:
|
|
|
|
optq = int(quality)
|
|
|
|
except ValueError:
|
|
|
|
raise error.UIException("Requested quality needs to be a number")
|
|
|
|
|
|
|
|
try:
|
2018-05-08 22:46:11 +02:00
|
|
|
optf = int(config.get("flexibleq"))
|
2018-03-13 00:33:39 +01:00
|
|
|
except ValueError:
|
|
|
|
raise error.UIException("Flexible-quality needs to be a number")
|
|
|
|
|
|
|
|
if optf == 0 and high:
|
|
|
|
optf = (high - quality) / 2
|
|
|
|
optq = quality + (high - quality) / 2
|
|
|
|
|
2020-07-28 21:30:24 +02:00
|
|
|
if config.get("format_preferred"):
|
|
|
|
form_prio = config.get("format_preferred").split(",")
|
|
|
|
else:
|
|
|
|
form_prio = DEFAULT_FORMAT_PRIO
|
|
|
|
streams = format_prio(streams, form_prio)
|
|
|
|
|
2021-05-16 02:22:37 +02:00
|
|
|
streams = audio_role(config, streams)
|
|
|
|
if not streams:
|
|
|
|
raise error.UIException(f"Can't find any streams with that audio role {config.get('audio_role')}")
|
|
|
|
|
|
|
|
streams = language_prio(config, streams)
|
|
|
|
if not streams:
|
|
|
|
raise error.UIException(f"Can't find any streams with that audio language {config.get('audio_language')}")
|
|
|
|
|
2022-05-13 08:36:34 +02:00
|
|
|
if config.get("resolution"):
|
2023-04-09 21:32:39 +02:00
|
|
|
resolutions = config.get("resolution").split(",")
|
2022-05-13 08:36:34 +02:00
|
|
|
streams = resolution(streams, resolutions)
|
|
|
|
if not streams:
|
|
|
|
raise error.UIException(f"Can't find any streams with that video resolution {config.get('resolution')}")
|
|
|
|
|
2021-05-09 01:16:16 +02:00
|
|
|
# Extract protocol prio, in the form of "hls,http",
|
2018-03-13 00:33:39 +01:00
|
|
|
# we want it as a list
|
|
|
|
|
2018-05-08 22:46:11 +02:00
|
|
|
if config.get("stream_prio"):
|
2019-08-25 00:27:31 +02:00
|
|
|
proto_prio = config.get("stream_prio").split(",")
|
2018-05-08 22:46:11 +02:00
|
|
|
elif config.get("live") or streams[0].config.get("live"):
|
2018-03-13 00:33:39 +01:00
|
|
|
proto_prio = LIVE_PROTOCOL_PRIO
|
|
|
|
else:
|
|
|
|
proto_prio = DEFAULT_PROTOCOL_PRIO
|
|
|
|
|
|
|
|
# Filter away any unwanted protocols, and prioritize
|
|
|
|
# based on --stream-priority.
|
|
|
|
streams = protocol_prio(streams, proto_prio)
|
|
|
|
|
|
|
|
if len(streams) == 0:
|
2019-09-06 22:49:49 +02:00
|
|
|
raise error.NoRequestedProtocols(requested=proto_prio, found=list({s.name for s in streams}))
|
2018-03-13 00:33:39 +01:00
|
|
|
|
|
|
|
# Build a dict indexed by bitrate, where each value
|
|
|
|
# is the stream with the highest priority protocol.
|
|
|
|
stream_hash = {}
|
|
|
|
for s in streams:
|
|
|
|
if s.bitrate not in stream_hash:
|
|
|
|
stream_hash[s.bitrate] = s
|
|
|
|
|
|
|
|
avail = sorted(stream_hash.keys(), reverse=True)
|
|
|
|
|
|
|
|
# wanted_lim is a two element tuple defines lower/upper bounds
|
|
|
|
# (inclusive). By default, we want only the best for you
|
|
|
|
# (literally!).
|
|
|
|
wanted_lim = (avail[0],) * 2
|
|
|
|
if optq:
|
|
|
|
wanted_lim = (optq - optf, optq + optf)
|
|
|
|
|
|
|
|
# wanted is the filtered list of available streams, having
|
|
|
|
# a bandwidth within the wanted_lim range.
|
|
|
|
wanted = [a for a in avail if a >= wanted_lim[0] and a <= wanted_lim[1]]
|
|
|
|
|
|
|
|
# If none remains, the bitrate filtering was too tight.
|
|
|
|
if len(wanted) == 0:
|
2021-05-16 21:40:01 +02:00
|
|
|
raise error.UIException("Can't find that quality. Try a different one listed in --list-quality or try --flexible-quality")
|
2018-03-13 00:33:39 +01:00
|
|
|
|
2018-05-08 22:46:11 +02:00
|
|
|
http = HTTP(config)
|
2018-03-13 00:33:39 +01:00
|
|
|
# Test if the wanted stream is available. If not try with the second best and so on.
|
|
|
|
for w in wanted:
|
2019-09-06 22:49:49 +02:00
|
|
|
res = http.get(stream_hash[w].url, cookies=stream_hash[w].kwargs.get("cookies", None))
|
2018-03-13 00:33:39 +01:00
|
|
|
if res is not None and res.status_code < 404:
|
|
|
|
return stream_hash[w]
|
|
|
|
|
2018-03-13 00:44:34 +01:00
|
|
|
raise error.UIException("Streams not available to download.")
|