urgoz

generic

Jun 26th, 2023 (edited)
177
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. """
  2.    generic streamlink plugin
  3.  
  4.    source: https://github.com/back-to/generic
  5.    issues: https://github.com/back-to/generic/issues
  6. """
  7. import base64
  8. import codecs
  9. import logging
  10. import os.path
  11. import re
  12.  
  13. from html import unescape as html_unescape
  14. from pathlib import Path
  15. from typing import Pattern
  16. from urllib.parse import parse_qsl, unquote, urljoin, urlparse
  17.  
  18. from streamlink.exceptions import (
  19.     FatalPluginError,
  20.     NoPluginError,
  21.     NoStreamsError,
  22. )
  23. from streamlink.plugin import Plugin, pluginargument, pluginmatcher
  24. from streamlink.plugin.api import useragents
  25. from streamlink.plugin.plugin import HIGH_PRIORITY
  26. from streamlink.stream import HLSStream, HTTPStream, DASHStream
  27. from streamlink.stream.ffmpegmux import MuxedStream
  28. from streamlink.utils.args import comma_list, num
  29. from streamlink.utils.url import update_scheme
  30.  
  31. try:
  32.     import yt_dlp as youtube_dl
  33.     HAS_YTDL = True
  34. except ImportError:
  35.     try:
  36.         import youtube_dl
  37.         HAS_YTDL = True
  38.     except ImportError:
  39.         HAS_YTDL = False
  40.  
  41. GENERIC_VERSION = "2022-09-02"
  42.  
  43. log = logging.getLogger(__name__)
  44.  
  45. obfuscatorhtml_chunk_re = re.compile(r'''["'](?P<chunk>[A-z0-9+/=]+)["']''')
  46. obfuscatorhtml_re = re.compile(
  47.     r'<script[^<>]*>[^<>]*var\s*(\w+)\s*=\s*\[(?P<chunks>[^\[\]]+)\];\s*\1\.forEach.*-\s*(?P<minus>\d+)[^<>]*</script>',
  48. )
  49. unpack_packer_re = re.compile(
  50.     r'''(?P<data>eval\(function\(p,a,c,k,e,(?:d|r)\).*\))''')
  51. unpack_unescape_re = re.compile(r"""
  52.    <script[^<>]*>[^>]*
  53.    document.write\(unescape\(\s*
  54.    ["']((?=[^<>"']*%\w{2})[^<>"']+)["']
  55.    \)\);?[^<]*</script>""", re.VERBOSE)
  56.  
  57. unpack_source_url_re_1 = re.compile(r'''(?x)source:\s*(?P<replace>window\.atob\(
  58.    (?P<q>["'])(?P<atob>[A-z0-9+/=]+)(?P=q)\)),\s*
  59.    mimeType:\s*["']application/vnd\.apple\.mpegurl["']
  60. ''')
  61. unpack_source_url_re_2 = re.compile(r'''(?x)var\s\w+url=(?P<replace>atob\(
  62.    (?P<q>["'])(?P<atob>[A-z0-9+/=]+)(?P=q)\));''')
  63. unpack_source_url_re_3 = re.compile(r'''(?x)Clappr\.Player\({\s*
  64.    source:\s*(?P<replace>atob\((?P<q>["'])(?P<atob>[A-z0-9+/=]+)(?P=q)\))''')
  65. unpack_u_m3u8_re = re.compile(r'(\\u0022[^\s,]+m3u8[^\s,]*\\u0022)')
  66.  
  67.  
  68. class UnpackingError(Exception):
  69.     """Badly packed source or general error."""
  70.  
  71.  
  72. class Packer(object):
  73.     """
  74.    Unpacker for Dean Edward's p.a.c.k.e.r
  75.  
  76.    source: https://github.com/beautify-web/js-beautify/
  77.    version: commit - b0e5f23a2d04db233f428349eb59e63bdefa78bb
  78.  
  79.    """
  80.  
  81.     def __init__(self):
  82.         self.beginstr = ''
  83.         self.endstr = ''
  84.  
  85.     def detect(self, source):
  86.         """Detects whether `source` is P.A.C.K.E.R. coded."""
  87.         mystr = source.replace(' ', '').find('eval(function(p,a,c,k,e,')
  88.         if(mystr > 0):
  89.             self.beginstr = source[:mystr]
  90.         if(mystr != -1):
  91.             """ Find endstr"""
  92.             if(source.split("')))", 1)[0] == source):
  93.                 try:
  94.                     self.endstr = source.split("}))", 1)[1]
  95.                 except IndexError:
  96.                     self.endstr = ''
  97.             else:
  98.                 self.endstr = source.split("')))", 1)[1]
  99.         return (mystr != -1)
  100.  
  101.     def unpack(self, source):
  102.         """Unpacks P.A.C.K.E.R. packed js code."""
  103.         payload, symtab, radix, count = self._filterargs(source)
  104.  
  105.         if count != len(symtab):
  106.             raise UnpackingError('Malformed p.a.c.k.e.r. symtab.')
  107.  
  108.         try:
  109.             if radix == 1:
  110.                 unbase = int
  111.             else:
  112.                 unbase = Unbaser(radix)
  113.         except TypeError:
  114.             raise UnpackingError('Unknown p.a.c.k.e.r. encoding.')
  115.  
  116.         def lookup(match):
  117.             """Look up symbols in the synthetic symtab."""
  118.             word = match.group(0)
  119.             return symtab[unbase(word)] or word
  120.  
  121.         source = re.sub(r'\b\w+\b', lookup, payload)
  122.         return self._replacestrings(source)
  123.  
  124.     def _filterargs(self, source):
  125.         """Juice from a source file the four args needed by decoder."""
  126.         juicers = [(r"}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\), *(\d+), *(.*)\)\)"),
  127.                    (r"}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\)"),
  128.                    ]
  129.         for juicer in juicers:
  130.             args = re.search(juicer, source, re.DOTALL)
  131.             if args:
  132.                 a = args.groups()
  133.                 if a[1] == "[]":
  134.                     a = list(a)
  135.                     a[1] = 62
  136.                     a = tuple(a)
  137.                 try:
  138.                     return a[0], a[3].split('|'), int(a[1]), int(a[2])
  139.                 except ValueError:
  140.                     raise UnpackingError('Corrupted p.a.c.k.e.r. data.')
  141.  
  142.         # could not find a satisfying regex
  143.         raise UnpackingError('Could not make sense of p.a.c.k.e.r data (unexpected code structure)')
  144.  
  145.     def _replacestrings(self, source):
  146.         """Strip string lookup table (list) and replace values in source."""
  147.         match = re.search(r'var *(_\w+)\=\["(.*?)"\];', source, re.DOTALL)
  148.  
  149.         if match:
  150.             varname, strings = match.groups()
  151.             startpoint = len(match.group(0))
  152.             lookup = strings.split('","')
  153.             variable = '%s[%%d]' % varname
  154.             for index, value in enumerate(lookup):
  155.                 source = source.replace(variable % index, '"%s"' % value)
  156.             return source[startpoint:]
  157.         return self.beginstr + source + self.endstr
  158.  
  159.  
  160. class Unbaser(object):
  161.     """Functor for a given base. Will efficiently convert
  162.    strings to natural numbers."""
  163.     ALPHABET = {
  164.         62: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
  165.         95: (' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  166.              '[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~')
  167.     }
  168.  
  169.     def __init__(self, base):
  170.         self.base = base
  171.         # fill elements 37...61, if necessary
  172.         if 36 < base < 62:
  173.             if not hasattr(self.ALPHABET,
  174.                            self.ALPHABET[62][:base]):
  175.                 self.ALPHABET[base] = self.ALPHABET[62][:base]
  176.         # attrs = self.ALPHABET
  177.         # print ', '.join("%s: %s" % item for item in attrs.items())
  178.         # If base can be handled by int() builtin, let it do it for us
  179.         if 2 <= base <= 36:
  180.             self.unbase = lambda s: int(s, base)
  181.         else:
  182.             # Build conversion dictionary cache
  183.             try:
  184.                 self.dictionary = dict(
  185.                     (cipher, index) for index, cipher in enumerate(self.ALPHABET[base]))
  186.             except KeyError:
  187.                 raise TypeError('Unsupported base encoding.')
  188.             self.unbase = self._dictunbaser
  189.  
  190.     def __call__(self, s):
  191.         return self.unbase(s)
  192.  
  193.     def _dictunbaser(self, s):
  194.         """Decodes a  value to an integer."""
  195.         ret = 0
  196.         for index, cipher in enumerate(s[::-1]):
  197.             ret += (self.base ** index) * self.dictionary[cipher]
  198.         return ret
  199.  
  200.  
  201. def unpack_packer(text: str) -> str:
  202.     """unpack p.a.c.k.e.r"""
  203.     packer = Packer()
  204.     packer_list = unpack_packer_re.findall(text)
  205.     if packer_list:
  206.         for data in packer_list:
  207.             if packer.detect(data):
  208.                 try:
  209.                     unpacked = packer.unpack(data).replace('\\', '')
  210.                     text = text.replace(data, unpacked)
  211.                 except UnpackingError:
  212.                     pass
  213.     return text
  214.  
  215.  
  216. def unpack_obfuscatorhtml(text: str) -> str:
  217.     """
  218.    Unpacker for Obfuscator HTML https://github.com/BlueEyesHF/Obfuscator-HTML
  219.    """
  220.     while True:
  221.         m = obfuscatorhtml_re.search(text)
  222.         if m:
  223.             unpacked = ""
  224.             chunks = obfuscatorhtml_chunk_re.findall(m.group('chunks'))
  225.             minus = int(m.group('minus'))
  226.             for chunk in chunks:
  227.                 int_chunk = int(re.sub(r'\D', '', str(base64.b64decode(chunk))))
  228.                 unpacked += chr(int_chunk - int(minus))
  229.             text = text.replace(m.group(0), unpacked)
  230.         else:
  231.             break
  232.     return text
  233.  
  234.  
  235. def unpack_unescape(text: str) -> str:
  236.     while True:
  237.         m = unpack_unescape_re.search(text)
  238.         if m:
  239.             text = text.replace(m.group(0), unquote(m.group(1)))
  240.         else:
  241.             break
  242.     return text
  243.  
  244.  
  245. def unpack_source_url(text: str, _unpack_source_url_re: Pattern) -> str:
  246.     while True:
  247.         m1 = _unpack_source_url_re.search(text)
  248.         if m1:
  249.             try:
  250.                 atob = base64.b64decode(m1.group("atob")).decode("utf-8")
  251.             except Exception:
  252.                 atob = 'INVALID unpack_source_url'
  253.  
  254.             try:
  255.                 atob = "{q}{atob}{q}".format(q=m1.group("q"), atob=atob)
  256.                 text = text.replace(m1.group("replace"), atob)
  257.             except Exception:
  258.                 pass
  259.         else:
  260.             break
  261.     return text
  262.  
  263.  
  264. def unpack_u_m3u8(text: str) -> str:
  265.     def _unicode_escape(s: str) -> str:
  266.         unicode_escape = codecs.getdecoder('unicode_escape')
  267.         return re.sub(r'\\u[0-9a-fA-F]{4}', lambda m: unicode_escape(m.group(0))[0], s)
  268.  
  269.     while True:
  270.         m = unpack_u_m3u8_re.search(text)
  271.         if m:
  272.             text = text.replace(m.group(0), _unicode_escape(m.group(0)))
  273.         else:
  274.             break
  275.     return text
  276.  
  277.  
  278. def unpack(text: str) -> str:
  279.     """ unpack html source code """
  280.     text = unpack_packer(text)
  281.     text = unpack_obfuscatorhtml(text)
  282.     text = unpack_unescape(text)
  283.     text = unpack_source_url(text, unpack_source_url_re_1)
  284.     text = unpack_source_url(text, unpack_source_url_re_2)
  285.     text = unpack_source_url(text, unpack_source_url_re_3)
  286.     text = unpack_u_m3u8(text)
  287.     return text
  288.  
  289.  
  290. class GenericCache(object):
  291.     '''GenericCache is useded as a temporary session cache
  292.       - GenericCache.blacklist_path
  293.       - GenericCache.cache_url_list
  294.       - GenericCache.whitelist_path
  295.    '''
  296.     pass
  297.  
  298.  
  299. @pluginmatcher(re.compile(r'((?:generic|resolve)://)(?P<url>.+)'), priority=HIGH_PRIORITY)
  300. @pluginmatcher(re.compile(r'(?P<url>.+)'), priority=1)
  301. @pluginargument(
  302.     "playlist-max",
  303.     default=5,
  304.     metavar="NUMBER",
  305.     type=num(int, ge=0, le=25),
  306.     help="""
  307.    Number of how many playlist URLs of the same type
  308.    are allowed to be resolved with this plugin.
  309.  
  310.    Default is 5"""
  311. )
  312. @pluginargument(
  313.     "playlist-referer",
  314.     metavar="URL",
  315.     help="""Set a custom referer URL for the playlist URLs.
  316.  
  317.    This only affects playlist URLs of this plugin.
  318.  
  319.    Default is the URL of the last website."""
  320. )
  321. @pluginargument(
  322.     "blacklist-netloc",
  323.     metavar="NETLOC",
  324.     type=comma_list,
  325.     help="""
  326.    Blacklist domains that should not be used,
  327.    by using a comma-separated list:
  328.  
  329.        "example.com,localhost,google.com"
  330.  
  331.    Useful for websites with a lot of iframes."""
  332. )
  333. @pluginargument(
  334.     "blacklist-path",
  335.     metavar="PATH",
  336.     type=comma_list,
  337.     help="""
  338.    Blacklist the path of a domain that should not be used,
  339.    by using a comma-separated list:
  340.  
  341.        "example.com/mypath,localhost/example,google.com/folder"
  342.  
  343.    Useful for websites with different iframes of the same domain.
  344.    """
  345. )
  346. @pluginargument(
  347.     "blacklist-filepath",
  348.     metavar="FILEPATH",
  349.     type=comma_list,
  350.     help="""
  351.    Blacklist file names for iframes and playlists
  352.    by using a comma-separated list:
  353.  
  354.        "index.html,ignore.m3u8,/ad/master.m3u8"
  355.  
  356.    Sometimes there are invalid URLs in the result list,
  357.    this can be used to remove them.
  358.    """
  359. )
  360. @pluginargument(
  361.     "whitelist-netloc",
  362.     metavar="NETLOC",
  363.     type=comma_list,
  364.     help="""
  365.    Whitelist domains that should only be searched for iframes,
  366.    by using a comma-separated list:
  367.  
  368.        "example.com,localhost,google.com"
  369.  
  370.    Useful for websites with lots of iframes,
  371.    where the main iframe always has the same hosting domain.
  372.    """
  373. )
  374. @pluginargument(
  375.     "whitelist-path",
  376.     metavar="PATH",
  377.     type=comma_list,
  378.     help="""
  379.    Whitelist the path of a domain that should only be searched
  380.    for iframes, by using a comma-separated list:
  381.  
  382.        "example.com/mypath,localhost/example,google.com/folder"
  383.  
  384.    Useful for websites with different iframes of the same domain,
  385.    where the main iframe always has the same path.
  386.    """
  387. )
  388. @pluginargument(
  389.     "ignore-same-url",
  390.     action="store_true",
  391.     help="""
  392.    Do not remove URLs from the valid list if they were already used.
  393.  
  394.    Sometimes needed as a workaround for --player-external-http issues.
  395.  
  396.    Be careful this might result in an infinity loop.
  397.    """
  398. )
  399. @pluginargument(
  400.     "ytdl-disable",
  401.     action="store_true",
  402.     help="Disable youtube-dl fallback."
  403. )
  404. @pluginargument(
  405.     "ytdl-only",
  406.     action="store_true",
  407.     help="""
  408.    Disable generic plugin and use only youtube-dl.
  409.    """
  410. )
  411. @pluginargument(
  412.     "debug",
  413.     action="store_true",
  414.     help="""
  415.    Developer Command!
  416.  
  417.    Saves unpacked HTML code of all opened URLs to the local hard drive for easier debugging.
  418.    """
  419. )
  420. class Generic(Plugin):
  421.     # iframes
  422.     _iframe_re = re.compile(r'''(?isx)
  423.        <ifr(?:["']\s?\+\s?["'])?ame
  424.        (?!\sname=["']g_iFrame).*?src=
  425.        ["'](?P<url>[^"'\s<>]+)\s?["']
  426.        [^<>]*?>
  427.    ''')
  428.     # playlists
  429.     _playlist_re = re.compile(r'''(?sx)
  430.        (?:["']|=|&quot;)(?P<url>
  431.            (?<!title=["'])
  432.            (?<!["']title["']:["'])
  433.                [^"'<>\s\;{}]+\.(?:m3u8|mp3|mp4|mpd)
  434.            (?:\?[^"'<>\s\\{}]+)?)/?
  435.        (?:\\?["']|(?<!;)\s|>|\\&quot;)
  436.    ''')
  437.     # mp3 and mp4 files
  438.     _httpstream_bitrate_re = re.compile(r'''(?x)
  439.        (?:_|\.|/|-)
  440.        (?:
  441.            (?P<bitrate>\d{1,4})(?:k)?
  442.            |
  443.            (?P<resolution>\d{1,4}p)
  444.            (?:\.h26(?:4|5))?
  445.        )
  446.        \.mp(?:3|4)
  447.    ''')
  448.     _httpstream_common_resolution_list = [
  449.         '2160', '1440', '1080', '720', '576', '480', '360', '240',
  450.     ]
  451.     # javascript redirection
  452.     _window_location_re = re.compile(r'''(?sx)
  453.        <script[^<]+window\.location\.href\s?=\s?["']
  454.        (?P<url>[^"']+)["'];[^<>]+
  455.    ''')
  456.     # obviously ad paths
  457.     _ads_path_re = re.compile(r'''(?x)
  458.        /ads?/?(?:\w+)?
  459.        (?:\d+x\d+)?
  460.        (?:_\w+)?\.(?:html?|php)$
  461.    ''')
  462.  
  463.     # START - _make_url_list
  464.     # Not allowed at the end of the parsed url path
  465.     blacklist_endswith = (
  466.         '.gif',
  467.         '.jpg',
  468.         '.png',
  469.         '.svg',
  470.         '.vtt',
  471.         '/chat.html',
  472.         '/chat',
  473.         '/novideo.mp4',
  474.         '/vidthumb.mp4',
  475.         '/ads-iframe-display.php',
  476.     )
  477.     # Not allowed at the end of the parsed url netloc
  478.     blacklist_netloc = (
  479.         '127.0.0.1',
  480.         'a.adtng.com',
  481.         'about:blank',
  482.         'abv.bg',
  483.         'adfox.ru',
  484.         'cbox.ws',
  485.         'googletagmanager.com',
  486.         'javascript:false',
  487.         'accounts.google.com',
  488.     )
  489.     # END - _make_url_list
  490.  
  491.     def __init__(self, *args, **kwargs):
  492.         super().__init__(*args, **kwargs)
  493.         self.url = update_scheme('http://', self.match.group('url'), force=False)
  494.         self.html_text = ''
  495.  
  496.         # START - cache every used url and set a referer
  497.         if hasattr(GenericCache, 'cache_url_list'):
  498.             GenericCache.cache_url_list += [self.url]
  499.             # set the last url as a referer
  500.             self.referer = GenericCache.cache_url_list[-2]
  501.         else:
  502.             GenericCache.cache_url_list = [self.url]
  503.             self.referer = self.url
  504.         self.session.http.headers.update({'Referer': self.referer})
  505.         # END
  506.  
  507.         # START - how often _get_streams already run
  508.         self._run = len(GenericCache.cache_url_list)
  509.         # END
  510.  
  511.     def compare_url_path(self, parsed_url, check_list,
  512.                          path_status='startswith'):
  513.         status = False
  514.         for netloc, path in check_list:
  515.             if path_status == '==':
  516.                 if (parsed_url.netloc.endswith(netloc) and parsed_url.path == path):
  517.                     status = True
  518.                     break
  519.             elif path_status == 'startswith':
  520.                 if (parsed_url.netloc.endswith(netloc) and parsed_url.path.startswith(path)):
  521.                     status = True
  522.                     break
  523.  
  524.         return status
  525.  
  526.     def merge_path_list(self, static, user):
  527.         for _path_url in user:
  528.             if not _path_url.startswith(('http', '//')):
  529.                 _path_url = update_scheme('http://', _path_url, force=False)
  530.             _parsed_path_url = urlparse(_path_url)
  531.             if _parsed_path_url.netloc and _parsed_path_url.path:
  532.                 static += [(_parsed_path_url.netloc, _parsed_path_url.path)]
  533.         return static
  534.  
  535.     def repair_url(self, url, base_url, stream_base=''):
  536.         # remove \
  537.         new_url = url.replace('\\', '')
  538.         # repairs broken scheme
  539.         if new_url.startswith('http&#58;//'):
  540.             new_url = 'http:' + new_url[9:]
  541.         elif new_url.startswith('https&#58;//'):
  542.             new_url = 'https:' + new_url[10:]
  543.         new_url = unquote(new_url)
  544.         # creates a valid url from path only urls
  545.         # and adds missing scheme for // urls
  546.         if stream_base and new_url[1] != '/':
  547.             if new_url[0] == '/':
  548.                 new_url = new_url[1:]
  549.             new_url = urljoin(stream_base, new_url)
  550.         else:
  551.             new_url = urljoin(base_url, new_url)
  552.         return new_url
  553.  
  554.     def _make_url_list(self, old_list, base_url, url_type=''):
  555.         # START - List for not allowed URL Paths
  556.         # --generic-blacklist-path
  557.         if not hasattr(GenericCache, 'blacklist_path'):
  558.  
  559.             # static list
  560.             blacklist_path = [
  561.                 ('facebook.com', '/connect'),
  562.                 ('facebook.com', '/plugins'),
  563.                 ('google.com', '/recaptcha/'),
  564.                 ('haber7.com', '/radyohome/station-widget/'),
  565.                 ('static.tvr.by', '/upload/video/atn/promo'),
  566.                 ('twitter.com', '/widgets'),
  567.                 ('vesti.ru', '/native_widget.html'),
  568.                 ('www.blogger.com', '/static'),
  569.                 ('youtube.com', '/['),
  570.             ]
  571.  
  572.             # merge user and static list
  573.             blacklist_path_user = self.get_option('blacklist_path')
  574.             if blacklist_path_user is not None:
  575.                 blacklist_path = self.merge_path_list(
  576.                     blacklist_path, blacklist_path_user)
  577.  
  578.             GenericCache.blacklist_path = blacklist_path
  579.         # END
  580.  
  581.         blacklist_path_same = [
  582.             ('player.vimeo.com', '/video/'),
  583.             ('youtube.com', '/embed/'),
  584.         ]
  585.  
  586.         # START - List of only allowed URL Paths for Iframes
  587.         # --generic-whitelist-path
  588.         if not hasattr(GenericCache, 'whitelist_path'):
  589.             whitelist_path = []
  590.             whitelist_path_user = self.get_option('whitelist_path')
  591.             if whitelist_path_user is not None:
  592.                 whitelist_path = self.merge_path_list(
  593.                     [], whitelist_path_user)
  594.             GenericCache.whitelist_path = whitelist_path
  595.         # END
  596.  
  597.         allow_same_url = (self.get_option('ignore_same_url'))
  598.  
  599.         new_list = []
  600.         for url in old_list:
  601.             new_url = self.repair_url(url, base_url)
  602.             # parse the url
  603.             parse_new_url = urlparse(new_url)
  604.  
  605.             # START
  606.             REMOVE = False
  607.             if new_url in GenericCache.cache_url_list and not allow_same_url:
  608.                 # Removes an already used url
  609.                 # ignored if --hls-session-reload is used
  610.                 REMOVE = 'SAME-URL'
  611.             elif (not parse_new_url.scheme.startswith(('http'))):
  612.                 # Allow only an url with a valid scheme
  613.                 REMOVE = 'SCHEME'
  614.             elif (url_type == 'iframe'
  615.                     and self.get_option('whitelist_netloc')
  616.                     and parse_new_url.netloc.endswith(tuple(self.get_option('whitelist_netloc'))) is False):
  617.                 # Allow only whitelisted domains for iFrames
  618.                 # --generic-whitelist-netloc
  619.                 REMOVE = 'WL-netloc'
  620.             elif (url_type == 'iframe'
  621.                     and GenericCache.whitelist_path
  622.                     and self.compare_url_path(parse_new_url, GenericCache.whitelist_path) is False):
  623.                 # Allow only whitelisted paths from a domain for iFrames
  624.                 # --generic-whitelist-path
  625.                 REMOVE = 'WL-path'
  626.             elif (parse_new_url.netloc.endswith(self.blacklist_netloc)):
  627.                 # Removes blacklisted domains from a static list
  628.                 # self.blacklist_netloc
  629.                 REMOVE = 'BL-static'
  630.             elif (self.get_option('blacklist_netloc')
  631.                   and parse_new_url.netloc.endswith(tuple(self.get_option('blacklist_netloc')))):
  632.                 # Removes blacklisted domains
  633.                 # --generic-blacklist-netloc
  634.                 REMOVE = 'BL-netloc'
  635.             elif (self.compare_url_path(parse_new_url, GenericCache.blacklist_path) is True):
  636.                 # Removes blacklisted paths from a domain
  637.                 # --generic-blacklist-path
  638.                 REMOVE = 'BL-path'
  639.             elif (parse_new_url.path.endswith(self.blacklist_endswith)):
  640.                 # Removes unwanted endswith images and chatrooms
  641.                 REMOVE = 'BL-ew'
  642.             elif (self.get_option('blacklist_filepath')
  643.                   and parse_new_url.path.endswith(tuple(self.get_option('blacklist_filepath')))):
  644.                 # Removes blacklisted file paths
  645.                 # --generic-blacklist-filepath
  646.                 REMOVE = 'BL-filepath'
  647.             elif (self._ads_path_re.search(parse_new_url.path) or parse_new_url.netloc.startswith(('ads.'))):
  648.                 # Removes obviously AD URL
  649.                 REMOVE = 'ADS'
  650.             elif (self.compare_url_path(parse_new_url, blacklist_path_same, path_status='==') is True):
  651.                 # Removes blacklisted same paths from a domain
  652.                 REMOVE = 'BL-path-same'
  653.             elif parse_new_url.netloc == 'cdn.embedly.com' and parse_new_url.path == '/widgets/media.html':
  654.                 # do not use the direct URL for 'cdn.embedly.com', search the query for a new URL
  655.                 params = dict(parse_qsl(parse_new_url.query))
  656.                 embedly_new_url = params.get('url') or params.get('src')
  657.                 if embedly_new_url:
  658.                     new_list += [embedly_new_url]
  659.                 else:
  660.                     log.error('Missing params URL or SRC for {0}'.format(new_url))
  661.                 continue
  662.             else:
  663.                 # valid URL
  664.                 new_list += [new_url]
  665.                 continue
  666.  
  667.             log.debug('{0} - Removed: {1}'.format(REMOVE, new_url))
  668.             # END
  669.  
  670.         # Remove duplicates
  671.         log.debug('List length: {0} (with duplicates)'.format(len(new_list)))
  672.         new_list = sorted(list(set(new_list)))
  673.         return new_list
  674.  
  675.     def _window_location(self):
  676.         match = self._window_location_re.search(self.html_text)
  677.         if match:
  678.             temp_url = urljoin(self.url, match.group('url'))
  679.             if temp_url not in GenericCache.cache_url_list:
  680.                 log.debug('Found window_location: {0}'.format(temp_url))
  681.                 return temp_url
  682.  
  683.         log.trace('No window_location')
  684.         return False
  685.  
  686.     def _resolve_playlist(self, playlist_all):
  687.         playlist_referer = self.get_option('playlist_referer') or self.url
  688.         self.session.http.headers.update({'Referer': playlist_referer})
  689.  
  690.         playlist_max = self.get_option('playlist_max') or 5
  691.         count_playlist = {
  692.             'dash': 0,
  693.             'hls': 0,
  694.             'http': 0,
  695.         }
  696.  
  697.         o = urlparse(self.url)
  698.         origin_tuple = (
  699.             '.cloudfront.net',
  700.         )
  701.  
  702.         for url in playlist_all:
  703.             parsed_url = urlparse(url)
  704.             if parsed_url.netloc.endswith(origin_tuple):
  705.                 self.session.http.headers.update({
  706.                     'Origin': '{0}://{1}'.format(o.scheme, o.netloc),
  707.                 })
  708.  
  709.             if (parsed_url.path.endswith(('.m3u8'))
  710.                     or parsed_url.query.endswith(('.m3u8'))):
  711.                 if count_playlist['hls'] >= playlist_max:
  712.                     log.debug('Skip - {0}'.format(url))
  713.                     continue
  714.                 try:
  715.                     streams = HLSStream.parse_variant_playlist(self.session, url).items()
  716.                     if not streams:
  717.                         yield 'live', HLSStream(self.session, url)
  718.                     for s in streams:
  719.                         yield s
  720.                     log.debug('HLS URL - {0}'.format(url))
  721.                     count_playlist['hls'] += 1
  722.                 except Exception as e:
  723.                     log.error('Skip HLS with error {0}'.format(str(e)))
  724.             elif (parsed_url.path.endswith(('.mp3', '.mp4'))
  725.                     or parsed_url.query.endswith(('.mp3', '.mp4'))):
  726.                 if count_playlist['http'] >= playlist_max:
  727.                     log.debug('Skip - {0}'.format(url))
  728.                     continue
  729.                 try:
  730.                     name = 'vod'
  731.                     m = self._httpstream_bitrate_re.search(url)
  732.                     if m:
  733.                         bitrate = m.group('bitrate')
  734.                         resolution = m.group('resolution')
  735.                         if bitrate:
  736.                             if bitrate in self._httpstream_common_resolution_list:
  737.                                 name = '{0}p'.format(m.group('bitrate'))
  738.                             else:
  739.                                 name = '{0}k'.format(m.group('bitrate'))
  740.                         elif resolution:
  741.                             name = resolution
  742.                     yield name, HTTPStream(self.session, url)
  743.                     log.debug('HTTP URL - {0}'.format(url))
  744.                     count_playlist['http'] += 1
  745.                 except Exception as e:
  746.                     log.error('Skip HTTP with error {0}'.format(str(e)))
  747.             elif (parsed_url.path.endswith(('.mpd'))
  748.                     or parsed_url.query.endswith(('.mpd'))):
  749.                 if count_playlist['dash'] >= playlist_max:
  750.                     log.debug('Skip - {0}'.format(url))
  751.                     continue
  752.                 try:
  753.                     for s in DASHStream.parse_manifest(self.session,
  754.                                                        url).items():
  755.                         yield s
  756.                     log.debug('DASH URL - {0}'.format(url))
  757.                     count_playlist['dash'] += 1
  758.                 except Exception as e:
  759.                     log.error('Skip DASH with error {0}'.format(str(e)))
  760.             else:
  761.                 log.error('parsed URL - {0}'.format(url))
  762.  
  763.     def _res_text(self, url):
  764.         try:
  765.             res = self.session.http.get(url, allow_redirects=True)
  766.         except Exception as e:
  767.             if 'Received response with content-encoding: gzip' in str(e):
  768.                 headers = {
  769.                     'User-Agent': useragents.FIREFOX,
  770.                     'Accept-Encoding': 'deflate'
  771.                 }
  772.                 res = self.session.http.get(url, headers=headers, allow_redirects=True)
  773.             elif '403 Client Error' in str(e):
  774.                 log.error('Website Access Denied/Forbidden, you might be geo-'
  775.                           'blocked or other params are missing.')
  776.                 raise NoStreamsError(self.url)
  777.             elif '404 Client Error' in str(e):
  778.                 log.error('Website was not found, the link is broken or dead.')
  779.                 raise NoStreamsError(self.url)
  780.             else:
  781.                 raise e
  782.  
  783.         if res.history:
  784.             for resp in res.history:
  785.                 log.debug('Redirect: {0} - {1}'.format(resp.status_code, resp.url))
  786.             log.debug('URL: {0}'.format(res.url))
  787.         return res.text
  788.  
  789.     def get_author(self):
  790.         parsed = urlparse(self.url)
  791.         split_username = list(filter(None, parsed.path.split('/')))
  792.         if len(split_username) == 1:
  793.             return split_username[0]
  794.         elif parsed.fragment:
  795.             return parsed.fragment
  796.         return super().get_author()
  797.  
  798.     def get_title(self):
  799.         if self.title is None:
  800.             if not self.html_text:
  801.                 self.html_text = self._res_text(self.url)
  802.             _og_title_re = re.compile(r'<meta\s*property="og:title"\s*content="(?P<title>[^<>]+)"\s*/?>')
  803.             _title_re = re.compile(r'<title[^<>]*>(?P<title>[^<>]+)</title>')
  804.             m = _og_title_re.search(self.html_text) or _title_re.search(self.html_text)
  805.             if m:
  806.                 self.title = re.sub(r'[\s]+', ' ', m.group('title'))
  807.                 self.title = re.sub(r'^\s*|\s*$', '', self.title)
  808.                 self.title = html_unescape(self.title)
  809.             if self.title is None:
  810.                 # fallback if there is no <title>
  811.                 self.title = self.url
  812.         return self.title
  813.  
  814.     def ytdl_fallback(self):
  815.         '''Basic support for m3u8 URLs with youtube-dl'''
  816.         log.debug(f'Fallback {youtube_dl.__name__} {youtube_dl.version.__version__}')
  817.  
  818.         class YTDL_Logger(object):
  819.             def debug(self, msg):
  820.                 log.debug(msg)
  821.  
  822.             def warning(self, msg):
  823.                 log.warning(msg)
  824.  
  825.             def error(self, msg):
  826.                 log.trace(msg)
  827.  
  828.         ydl_opts = {
  829.             'call_home': False,
  830.             'forcejson': True,
  831.             'logger': YTDL_Logger(),
  832.             'no_color': True,
  833.             'noplaylist': True,
  834.             'no_warnings': True,
  835.             'verbose': False,
  836.             'quiet': True,
  837.         }
  838.  
  839.         with youtube_dl.YoutubeDL(ydl_opts) as ydl:
  840.             try:
  841.                 info = ydl.extract_info(self.url, download=False)
  842.             except Exception:
  843.                 return
  844.  
  845.             if not info or not info.get('formats'):
  846.                 return
  847.  
  848.         self.title = info['title']
  849.  
  850.         streams = []
  851.         for stream in info['formats']:
  852.             if stream['protocol'] in ['m3u8', 'm3u8_native'] and stream['ext'] == 'mp4':
  853.                 log.trace('{0!r}'.format(stream))
  854.                 name = stream.get('height') or stream.get('width')
  855.                 if name:
  856.                     name = '{0}p'.format(name)
  857.                     streams.append((name, HLSStream(self.session,
  858.                                                     stream['url'],
  859.                                                     headers=stream['http_headers'])))
  860.  
  861.         if not streams:
  862.             if ('youtube.com' in self.url
  863.                     and info.get('requested_formats')
  864.                     and len(info.get('requested_formats')) == 2
  865.                     and MuxedStream.is_usable(self.session)):
  866.                 audio_url = audio_format = video_url = video_format = video_name = None
  867.                 for stream in info.get('requested_formats'):
  868.                     if stream.get('format_id') == '135':
  869.                         url = stream.get('manifest_url')
  870.                         if not url:
  871.                             return
  872.                         return DASHStream.parse_manifest(self.session, url).items()
  873.                     if not stream.get('height'):
  874.                         audio_url = stream.get('url')
  875.                         audio_format = stream.get('format_id')
  876.                     if stream.get('height'):
  877.                         video_url = stream.get('url')
  878.                         video_format = stream.get('format_id')
  879.                         video_name = '{0}p'.format(stream.get('height'))
  880.  
  881.                 log.debug('MuxedStream: v {video} a {audio} = {name}'.format(
  882.                     audio=audio_format,
  883.                     name=video_name,
  884.                     video=video_format,
  885.                 ))
  886.                 streams.append((video_name,
  887.                                 MuxedStream(self.session,
  888.                                             HTTPStream(self.session, video_url, headers=stream['http_headers']),
  889.                                             HTTPStream(self.session, audio_url, headers=stream['http_headers']))
  890.                                 ))
  891.         return streams
  892.  
  893.     def _get_streams(self):
  894.         if HAS_YTDL and not self.get_option('ytdl-disable') and self.get_option('ytdl-only'):
  895.             ___streams = self.ytdl_fallback()
  896.             if ___streams and len(___streams) >= 1:
  897.                 return (s for s in ___streams)
  898.             if self.get_option('ytdl-only'):
  899.                 return
  900.  
  901.         if self._run <= 1:
  902.             log.info('Version {0} - https://github.com/back-to/generic'.format(GENERIC_VERSION))
  903.  
  904.         new_url = False
  905.         log.info('  {0}. URL={1}'.format(self._run, self.url))
  906.  
  907.         # GET website content
  908.         self.html_text = self._res_text(self.url)
  909.         # unpack common javascript codes
  910.         self.html_text = unpack(self.html_text)
  911.  
  912.         if self.get_option('debug'):
  913.             _valid_filepath = re.sub(r'(?u)[^-\w.]', '', str(self.url).strip().replace(' ', '_'))
  914.             _new_file = os.path.join(Path().absolute(),
  915.                                      f'{self._run}_{_valid_filepath}.html')
  916.             log.warning(f'NEW DEBUG FILE! {_new_file}')
  917.             try:
  918.                 with open(_new_file, 'w+') as f:
  919.                     f.write(str(self.html_text))
  920.             except OSError:
  921.                 pass
  922.  
  923.         # Playlist URL
  924.         playlist_all = self._playlist_re.findall(self.html_text)
  925.         if playlist_all:
  926.             log.debug('Found Playlists: {0}'.format(len(playlist_all)))
  927.             playlist_list = self._make_url_list(playlist_all,
  928.                                                 self.url,
  929.                                                 url_type='playlist',
  930.                                                 )
  931.             if playlist_list:
  932.                 log.info('Found Playlists: {0} (valid)'.format(
  933.                     len(playlist_list)))
  934.                 return self._resolve_playlist(playlist_list)
  935.         else:
  936.             log.trace('No Playlists')
  937.  
  938.         # iFrame URL
  939.         iframe_list = self._iframe_re.findall(self.html_text)
  940.         if iframe_list:
  941.             log.debug('Found Iframes: {0}'.format(len(iframe_list)))
  942.             # repair and filter iframe url list
  943.             new_iframe_list = self._make_url_list(iframe_list,
  944.                                                   self.url,
  945.                                                   url_type='iframe')
  946.             if new_iframe_list:
  947.                 number_iframes = len(new_iframe_list)
  948.                 if number_iframes == 1:
  949.                     new_url = new_iframe_list[0]
  950.                 else:
  951.                     log.info('--- IFRAMES ---')
  952.                     for i, item in enumerate(new_iframe_list, start=1):
  953.                         log.info('{0} - {1}'.format(i, item))
  954.                     log.info('--- IFRAMES ---')
  955.  
  956.                     try:
  957.                         number = int(self.input_ask(
  958.                             'Choose an iframe number from above').split(' ')[0])
  959.                         new_url = new_iframe_list[number - 1]
  960.                     except FatalPluginError:
  961.                         new_url = new_iframe_list[0]
  962.                     except ValueError:
  963.                         log.error('invalid input answer')
  964.                     except (IndexError, TypeError):
  965.                         log.error('invalid input number')
  966.  
  967.                     if not new_url:
  968.                         new_url = new_iframe_list[0]
  969.         else:
  970.             log.trace('No iframes')
  971.  
  972.         if not new_url:
  973.             # search for window.location.href
  974.             new_url = self._window_location()
  975.  
  976.         if new_url:
  977.             # the Dailymotion Plugin does not work with this Referer
  978.             if 'dailymotion.com' in new_url:
  979.                 del self.session.http.headers['Referer']
  980.             return self.session.streams(new_url)
  981.  
  982.         if HAS_YTDL and not self.get_option('ytdl-disable') and not self.get_option('ytdl-only'):
  983.             ___streams = self.ytdl_fallback()
  984.             if ___streams and len(___streams) >= 1:
  985.                 return (s for s in ___streams)
  986.  
  987.         raise NoPluginError
  988.  
  989.  
  990. __plugin__ = Generic
  991.  
Add Comment
Please, Sign In to add comment