class TestPluginAkamaiHDPlugin(unittest.TestCase): def setUp(self): self.session = Livecli() def test_can_handle_url(self): should_match = [ "akamaihd://https://example.com/index.mp3", "akamaihd://https://example.com/index.mp4", ] for url in should_match: self.assertTrue(AkamaiHDPlugin.can_handle_url(url)) should_not_match = [ "https://example.com/index.html", ] for url in should_not_match: self.assertFalse(AkamaiHDPlugin.can_handle_url(url)) def _test_akamaihd(self, surl, url): plugin = self.session.resolve_url(surl) streams = plugin.streams() self.assertTrue("live" in streams) stream = streams["live"] self.assertTrue(isinstance(stream, AkamaiHDStream)) self.assertEqual(stream.url, url) def test_plugin_akamaihd(self): self._test_akamaihd("akamaihd://http://hostname.se/stream", "http://hostname.se/stream") self._test_akamaihd("akamaihd://hostname.se/stream", "http://hostname.se/stream")
class TestPluginRTMPPlugin(unittest.TestCase): def setUp(self): self.session = Livecli() def assertDictHas(self, a, b): for key, value in a.items(): self.assertEqual(b[key], value) def test_can_handle_url(self): should_match = [ "rtmp://https://example.com/", "rtmpe://https://example.com/", "rtmps://https://example.com/", "rtmpt://https://example.com/", "rtmpte://https://example.com/", ] for url in should_match: self.assertTrue(RTMPPlugin.can_handle_url(url)) should_not_match = [ "https://example.com/index.html", ] for url in should_not_match: self.assertFalse(RTMPPlugin.can_handle_url(url)) def _test_rtmp(self, surl, url, params): plugin = self.session.resolve_url(surl) streams = plugin.streams() self.assertTrue("live" in streams) stream = streams["live"] self.assertTrue(isinstance(stream, RTMPStream)) self.assertEqual(stream.params["rtmp"], url) self.assertDictHas(params, stream.params) def test_plugin_rtmp(self): self._test_rtmp("rtmp://hostname.se/stream", "rtmp://hostname.se/stream", dict()) self._test_rtmp("rtmp://hostname.se/stream live=1 qarg='a \\'string' noq=test", "rtmp://hostname.se/stream", dict(live=True, qarg='a \'string', noq="test")) self._test_rtmp("rtmp://hostname.se/stream live=1 num=47", "rtmp://hostname.se/stream", dict(live=True, num=47)) self._test_rtmp("rtmp://hostname.se/stream conn=['B:1','S:authMe','O:1','NN:code:1.23','NS:flag:ok','O:0']", "rtmp://hostname.se/stream", dict(conn=['B:1', 'S:authMe', 'O:1', 'NN:code:1.23', 'NS:flag:ok', 'O:0']))
class TestPluginHTTPStreamPlugin(unittest.TestCase): def setUp(self): self.session = Livecli() def assertDictHas(self, a, b): for key, value in a.items(): self.assertEqual(b[key], value) def test_can_handle_url(self): should_match = [ "httpstream://https://example.com/index.mp3", "httpstream://https://example.com/index.mp4", ] for url in should_match: self.assertTrue(HTTPStreamPlugin.can_handle_url(url)) should_not_match = [ "https://example.com/index.html", ] for url in should_not_match: self.assertFalse(HTTPStreamPlugin.can_handle_url(url)) def _test_http(self, surl, url, params): plugin = self.session.resolve_url(surl) streams = plugin.streams() self.assertTrue("live" in streams) stream = streams["live"] self.assertTrue(isinstance(stream, HTTPStream)) self.assertEqual(stream.url, url) self.assertDictHas(params, stream.args) def test_plugin_http(self): self._test_http( "httpstream://http://hostname.se/auth.php auth=('test','test2')", "http://hostname.se/auth.php", dict(auth=("test", "test2"))) self._test_http( "httpstream://hostname.se/auth.php auth=('test','test2')", "http://hostname.se/auth.php", dict(auth=("test", "test2"))) self._test_http( "httpstream://https://hostname.se/auth.php verify=False params={'key': 'a value'}", "https://hostname.se/auth.php?key=a+value", dict(verify=False, params=dict(key='a value')))
class TestSession(unittest.TestCase): PluginPath = os.path.join(os.path.dirname(__file__), "plugins") def setUp(self): self.session = Livecli() self.session.load_plugins(self.PluginPath) def test_exceptions(self): try: # Turn off the resolve.py plugin self.session.set_plugin_option("resolve", "turn_off", True) self.session.resolve_url("invalid url") self.assertTrue(False) except NoPluginError: self.assertTrue(True) def test_load_plugins(self): plugins = self.session.get_plugins() self.assertTrue(plugins["testplugin"]) def test_builtin_plugins(self): plugins = self.session.get_plugins() self.assertTrue("twitch" in plugins) def test_resolve_url(self): plugins = self.session.get_plugins() plugin = self.session.resolve_url("http://test.se/channel") self.assertTrue(isinstance(plugin, Plugin)) self.assertTrue(isinstance(plugin, plugins["testplugin"])) def test_resolve_url_priority(self): from tests.plugins.testplugin import TestPlugin class HighPriority(TestPlugin): @classmethod def priority(cls, url): return HIGH_PRIORITY class LowPriority(TestPlugin): @classmethod def priority(cls, url): return LOW_PRIORITY self.session.plugins = { "test_plugin": TestPlugin, "test_plugin_low": LowPriority, "test_plugin_high": HighPriority, } plugin = self.session.resolve_url_no_redirect("http://test.se/channel") plugins = self.session.get_plugins() self.assertTrue(isinstance(plugin, plugins["test_plugin_high"])) self.assertEqual(HIGH_PRIORITY, plugin.priority(plugin.url)) def test_resolve_url_no_redirect(self): plugins = self.session.get_plugins() plugin = self.session.resolve_url_no_redirect("http://test.se/channel") self.assertTrue(isinstance(plugin, Plugin)) self.assertTrue(isinstance(plugin, plugins["testplugin"])) def test_options(self): self.session.set_option("test_option", "option") self.assertEqual(self.session.get_option("test_option"), "option") self.assertEqual(self.session.get_option("non_existing"), None) self.assertEqual(self.session.get_plugin_option("testplugin", "a_option"), "default") self.session.set_plugin_option("testplugin", "another_option", "test") self.assertEqual(self.session.get_plugin_option("testplugin", "another_option"), "test") self.assertEqual(self.session.get_plugin_option("non_existing", "non_existing"), None) self.assertEqual(self.session.get_plugin_option("testplugin", "non_existing"), None) def test_plugin(self): plugin = self.session.resolve_url("http://test.se/channel") streams = plugin.streams() self.assertTrue("best" in streams) self.assertTrue("worst" in streams) self.assertTrue(streams["best"] is streams["1080p"]) self.assertTrue(streams["worst"] is streams["350k"]) self.assertTrue(isinstance(streams["rtmp"], RTMPStream)) self.assertTrue(isinstance(streams["http"], HTTPStream)) self.assertTrue(isinstance(streams["hls"], HLSStream)) self.assertTrue(isinstance(streams["akamaihd"], AkamaiHDStream)) def test_plugin_stream_types(self): plugin = self.session.resolve_url("http://test.se/channel") streams = plugin.streams(stream_types=["http", "rtmp"]) self.assertTrue(isinstance(streams["480p"], HTTPStream)) self.assertTrue(isinstance(streams["480p_rtmp"], RTMPStream)) streams = plugin.streams(stream_types=["rtmp", "http"]) self.assertTrue(isinstance(streams["480p"], RTMPStream)) self.assertTrue(isinstance(streams["480p_http"], HTTPStream)) def test_plugin_stream_sorted_excludes(self): plugin = self.session.resolve_url("http://test.se/channel") streams = plugin.streams(sorting_excludes=["1080p", "3000k"]) self.assertTrue("best" in streams) self.assertTrue("worst" in streams) self.assertTrue(streams["best"] is streams["1500k"]) streams = plugin.streams(sorting_excludes=[">=1080p", ">1500k"]) self.assertTrue(streams["best"] is streams["1500k"]) streams = plugin.streams(sorting_excludes=lambda q: not q.endswith("p")) self.assertTrue(streams["best"] is streams["3000k"]) def test_plugin_support(self): plugin = self.session.resolve_url("http://test.se/channel") streams = plugin.streams() self.assertTrue("support" in streams) self.assertTrue(isinstance(streams["support"], HTTPStream)) def test_version(self): # PEP440 - https://www.python.org/dev/peps/pep-0440/ VERSION_PATTERN = r""" v? (?: (?:(?P<epoch>[0-9]+)!)? # epoch (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment (?P<pre> # pre-release [-_\.]? (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview)) [-_\.]? (?P<pre_n>[0-9]+)? )? (?P<post> # post release (?:-(?P<post_n1>[0-9]+)) | (?: [-_\.]? (?P<post_l>post|rev|r) [-_\.]? (?P<post_n2>[0-9]+)? ) )? (?P<dev> # dev release [-_\.]? (?P<dev_l>dev) [-_\.]? (?P<dev_n>[0-9]+)? )? ) (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version """ _version_re = re.compile( r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE, ) self.assertRegexpMatches(self.session.version, _version_re)
def _play_stream(HTTPBase, redirect=False): """Creates a livecli session and plays the stream.""" session = Livecli() session.set_logprefix("[ID-{0}]".format(str(int(time()))[4:])) logger = session.logger.new_module("livecli-server") session.set_loglevel("info") logger.info("User-Agent: {0}".format(HTTPBase.headers.get("User-Agent", "???"))) logger.info("Client: {0}".format(HTTPBase.client_address)) logger.info("Address: {0}".format(HTTPBase.address_string())) # Load custom user plugins if os.path.isdir(PLUGINS_DIR): session.load_plugins(PLUGINS_DIR) old_data = parse_qsl(urlparse(HTTPBase.path).query) data = [] for k, v in old_data: data += [(unquote_plus(k), unquote_plus(v))] data_other, session = command_session(session, data) url = data_other.get("url") if not url: HTTPBase._headers(404, "text/html") logger.error("No URL provided.") return plugin = session.resolve_url(url) logger.info("Found matching plugin {0} for URL {1}", plugin.module, url) # set cache size try: cache = data_other.get("cache") or 4096 except TypeError: cache = 4096 # set loglevel loglevel = data_other.get("l") or data_other.get("loglevel") or "debug" session.set_loglevel(loglevel) # find streams try: if redirect is True: streams = session.streams(url, stream_types=["hls", "http"]) elif data_other.get("stream-types"): streams = session.streams(url, stream_types=data_other.get("stream-types")) else: streams = session.streams(url) except Exception as e: HTTPBase._headers(404, "text/html") logger.error("No Stream Found!") return if not streams: HTTPBase._headers(404, "text/html") return # set quality quality = (data_other.get("q") or data_other.get("quality") or data_other.get("stream") or data_other.get("default-stream") or ["best"]) stream_name = "best" validstreams = format_valid_streams(plugin, streams) for stream_name in quality: if stream_name in streams: logger.info("Available streams: {0}", validstreams) stream_name = resolve_stream_name(streams, stream_name) break try: stream = streams[stream_name] logger.debug("Valid quality: {0}".format(stream_name)) except KeyError: logger.debug("Invald quality: '{0}', using 'best' instead".format(stream_name)) stream = streams["best"] if not isinstance(stream, (HDSStream, HTTPStream, MuxedStream)): # allow only http based streams: HDS HLS HTTP # RTMP is not supported logger.debug("only HTTP, HLS, HDS or MuxedStreams are supported.") logger.debug(str(type(stream))) HTTPBase._headers(404, "text/html") return if redirect is True: logger.info("301 - URL: {0}".format(stream.url)) HTTPBase.send_response(301) HTTPBase.send_header("Location", stream.url) HTTPBase.end_headers() logger.info("301 - done") return hls_session_reload = data_other.get("hls-session-reload") if hls_session_reload: livecli_cache = Cache( filename="streamdata.json", key_prefix="cache:{0}".format(stream.url) ) livecli_cache.set("cache_stream_name", stream_name, (int(hls_session_reload) + 60)) livecli_cache.set("cache_url", url, (int(hls_session_reload) + 60)) session.set_option("hls-session-reload", int(hls_session_reload)) try: fd = stream.open() except StreamError as err: HTTPBase._headers(404, "text/html") logger.error("Could not open stream: {0}".format(err)) return HTTPBase._headers(200, "video/unknown") try: logger.debug("Pre-buffering {0} bytes".format(cache)) while True: buff = fd.read(cache) if not buff: logger.error("No Data!") break HTTPBase.wfile.write(buff) HTTPBase.wfile.close() except socket.error as e: if isinstance(e.args, tuple): if e.errno == errno.EPIPE: # remote peer disconnected logger.info("Detected remote disconnect") pass else: logger.error(str(e)) else: logger.error(str(e)) fd.close() logger.info("Stream ended") fd = None
class TestPluginHLSPlugin(unittest.TestCase): def setUp(self): self.session = Livecli() def test_can_handle_url(self): should_match = [ "https://example.com/index.m3u8", "https://example.com/index.m3u8?test=true", "hls://*****:*****@patch('livecli.stream.HLSStream.parse_variant_playlist') def _test_hls(self, surl, url, mock_parse): mock_parse.return_value = {} plugin = self.session.resolve_url(surl) streams = plugin.streams() self.assertTrue("live" in streams) mock_parse.assert_called_with(self.session, url) stream = streams["live"] self.assertTrue(isinstance(stream, HLSStream)) self.assertEqual(stream.url, url) @patch('livecli.stream.HLSStream.parse_variant_playlist') def _test_hlsvariant(self, surl, url, mock_parse): mock_parse.return_value = {"best": HLSStream(self.session, url)} plugin = self.session.resolve_url(surl) streams = plugin.streams() mock_parse.assert_called_with(self.session, url) self.assertFalse("live" in streams) self.assertTrue("best" in streams) stream = streams["best"] self.assertTrue(isinstance(stream, HLSStream)) self.assertEqual(stream.url, url) def test_plugin_hls(self): self._test_hls("hls://https://hostname.se/playlist.m3u8", "https://hostname.se/playlist.m3u8") self._test_hls("hls://hostname.se/playlist.m3u8", "http://hostname.se/playlist.m3u8") self._test_hlsvariant("hls://hostname.se/playlist.m3u8", "http://hostname.se/playlist.m3u8") self._test_hlsvariant("hls://https://hostname.se/playlist.m3u8", "https://hostname.se/playlist.m3u8")
class TestSession(unittest.TestCase): PluginPath = os.path.join(os.path.dirname(__file__), "plugins") def setUp(self): self.session = Livecli() self.session.load_plugins(self.PluginPath) # def test_exceptions(self): # try: # self.session.resolve_url("invalid url") # self.assertTrue(False) # except NoPluginError: # self.assertTrue(True) def test_load_plugins(self): plugins = self.session.get_plugins() self.assertTrue(plugins["testplugin"]) def test_builtin_plugins(self): plugins = self.session.get_plugins() self.assertTrue("twitch" in plugins) def test_resolve_url(self): plugins = self.session.get_plugins() channel = self.session.resolve_url("http://test.se/channel") self.assertTrue(isinstance(channel, Plugin)) self.assertTrue(isinstance(channel, plugins["testplugin"])) def test_resolve_url_priority(self): from tests.plugins.testplugin import TestPlugin class HighPriority(TestPlugin): @classmethod def priority(cls, url): return HIGH_PRIORITY class LowPriority(TestPlugin): @classmethod def priority(cls, url): return LOW_PRIORITY self.session.plugins = { "test_plugin": TestPlugin, "test_plugin_low": LowPriority, "test_plugin_high": HighPriority, } channel = self.session.resolve_url_no_redirect("http://test.se/channel") plugins = self.session.get_plugins() self.assertTrue(isinstance(channel, plugins["test_plugin_high"])) self.assertEqual(HIGH_PRIORITY, channel.priority(channel.url)) def test_resolve_url_no_redirect(self): plugins = self.session.get_plugins() channel = self.session.resolve_url_no_redirect("http://test.se/channel") self.assertTrue(isinstance(channel, Plugin)) self.assertTrue(isinstance(channel, plugins["testplugin"])) def test_options(self): self.session.set_option("test_option", "option") self.assertEqual(self.session.get_option("test_option"), "option") self.assertEqual(self.session.get_option("non_existing"), None) self.assertEqual(self.session.get_plugin_option("testplugin", "a_option"), "default") self.session.set_plugin_option("testplugin", "another_option", "test") self.assertEqual(self.session.get_plugin_option("testplugin", "another_option"), "test") self.assertEqual(self.session.get_plugin_option("non_existing", "non_existing"), None) self.assertEqual(self.session.get_plugin_option("testplugin", "non_existing"), None) def test_plugin(self): channel = self.session.resolve_url("http://test.se/channel") streams = channel.get_streams() self.assertTrue("best" in streams) self.assertTrue("worst" in streams) self.assertTrue(streams["best"] is streams["1080p"]) self.assertTrue(streams["worst"] is streams["350k"]) self.assertTrue(isinstance(streams["rtmp"], RTMPStream)) self.assertTrue(isinstance(streams["http"], HTTPStream)) self.assertTrue(isinstance(streams["hls"], HLSStream)) self.assertTrue(isinstance(streams["akamaihd"], AkamaiHDStream)) def test_plugin_stream_types(self): channel = self.session.resolve_url("http://test.se/channel") streams = channel.get_streams(stream_types=["http", "rtmp"]) self.assertTrue(isinstance(streams["480p"], HTTPStream)) self.assertTrue(isinstance(streams["480p_rtmp"], RTMPStream)) streams = channel.get_streams(stream_types=["rtmp", "http"]) self.assertTrue(isinstance(streams["480p"], RTMPStream)) self.assertTrue(isinstance(streams["480p_http"], HTTPStream)) def test_plugin_stream_sorted_excludes(self): channel = self.session.resolve_url("http://test.se/channel") streams = channel.get_streams(sorting_excludes=["1080p", "3000k"]) self.assertTrue("best" in streams) self.assertTrue("worst" in streams) self.assertTrue(streams["best"] is streams["1500k"]) streams = channel.get_streams(sorting_excludes=[">=1080p", ">1500k"]) self.assertTrue(streams["best"] is streams["1500k"]) streams = channel.get_streams(sorting_excludes=lambda q: not q.endswith("p")) self.assertTrue(streams["best"] is streams["3000k"]) def test_plugin_support(self): channel = self.session.resolve_url("http://test.se/channel") streams = channel.get_streams() self.assertTrue("support" in streams) self.assertTrue(isinstance(streams["support"], HTTPStream))
class TestPluginStream(unittest.TestCase): def setUp(self): self.session = Livecli() def assertDictHas(self, a, b): for key, value in a.items(): self.assertEqual(b[key], value) def _test_akamaihd(self, surl, url): channel = self.session.resolve_url(surl) streams = channel.get_streams() self.assertTrue("live" in streams) stream = streams["live"] self.assertTrue(isinstance(stream, AkamaiHDStream)) self.assertEqual(stream.url, url) @patch('livecli.stream.HLSStream.parse_variant_playlist') def _test_hls(self, surl, url, mock_parse): mock_parse.return_value = {} channel = self.session.resolve_url(surl) streams = channel.get_streams() self.assertTrue("live" in streams) mock_parse.assert_called_with(self.session, url) stream = streams["live"] self.assertTrue(isinstance(stream, HLSStream)) self.assertEqual(stream.url, url) @patch('livecli.stream.HLSStream.parse_variant_playlist') def _test_hlsvariant(self, surl, url, mock_parse): mock_parse.return_value = {"best": HLSStream(self.session, url)} channel = self.session.resolve_url(surl) streams = channel.get_streams() mock_parse.assert_called_with(self.session, url) self.assertFalse("live" in streams) self.assertTrue("best" in streams) stream = streams["best"] self.assertTrue(isinstance(stream, HLSStream)) self.assertEqual(stream.url, url) def _test_rtmp(self, surl, url, params): channel = self.session.resolve_url(surl) streams = channel.get_streams() self.assertTrue("live" in streams) stream = streams["live"] self.assertTrue(isinstance(stream, RTMPStream)) self.assertEqual(stream.params["rtmp"], url) self.assertDictHas(params, stream.params) def _test_http(self, surl, url, params): channel = self.session.resolve_url(surl) streams = channel.get_streams() self.assertTrue("live" in streams) stream = streams["live"] self.assertTrue(isinstance(stream, HTTPStream)) self.assertEqual(stream.url, url) self.assertDictHas(params, stream.args) def test_plugin_rtmp(self): self._test_rtmp("rtmp://hostname.se/stream", "rtmp://hostname.se/stream", dict()) self._test_rtmp("rtmp://hostname.se/stream live=1 qarg='a \\'string' noq=test", "rtmp://hostname.se/stream", dict(live=True, qarg='a \'string', noq="test")) self._test_rtmp("rtmp://hostname.se/stream live=1 num=47", "rtmp://hostname.se/stream", dict(live=True, num=47)) self._test_rtmp("rtmp://hostname.se/stream conn=['B:1','S:authMe','O:1','NN:code:1.23','NS:flag:ok','O:0']", "rtmp://hostname.se/stream", dict(conn=['B:1', 'S:authMe', 'O:1', 'NN:code:1.23', 'NS:flag:ok', 'O:0'])) def test_plugin_hls(self): self._test_hls("hls://https://hostname.se/playlist.m3u8", "https://hostname.se/playlist.m3u8") self._test_hls("hls://hostname.se/playlist.m3u8", "http://hostname.se/playlist.m3u8") self._test_hlsvariant("hls://hostname.se/playlist.m3u8", "http://hostname.se/playlist.m3u8") self._test_hlsvariant("hls://https://hostname.se/playlist.m3u8", "https://hostname.se/playlist.m3u8") def test_plugin_akamaihd(self): self._test_akamaihd("akamaihd://http://hostname.se/stream", "http://hostname.se/stream") self._test_akamaihd("akamaihd://hostname.se/stream", "http://hostname.se/stream") def test_plugin_http(self): self._test_http("httpstream://http://hostname.se/auth.php auth=('test','test2')", "http://hostname.se/auth.php", dict(auth=("test", "test2"))) self._test_http("httpstream://hostname.se/auth.php auth=('test','test2')", "http://hostname.se/auth.php", dict(auth=("test", "test2"))) self._test_http("httpstream://https://hostname.se/auth.php verify=False params={'key': 'a value'}", "https://hostname.se/auth.php?key=a+value", dict(verify=False, params=dict(key='a value'))) def test_parse_params(self): self.assertEqual( dict(verify=False, params=dict(key="a value")), parse_params("""verify=False params={'key': 'a value'}""") ) self.assertEqual( dict(verify=False), parse_params("""verify=False""") ) self.assertEqual( dict(conn=['B:1', 'S:authMe', 'O:1', 'NN:code:1.23', 'NS:flag:ok', 'O:0']), parse_params(""""conn=['B:1', 'S:authMe', 'O:1', 'NN:code:1.23', 'NS:flag:ok', 'O:0']""") ) def test_stream_weight(self): self.assertEqual( (720, "pixels"), stream_weight("720p")) self.assertEqual( (721, "pixels"), stream_weight("720p+")) self.assertEqual( (780, "pixels"), stream_weight("720p60")) self.assertTrue( stream_weight("720p+") > stream_weight("720p")) self.assertTrue( stream_weight("720p") == stream_weight("720p")) self.assertTrue( stream_weight("720p_3000k") > stream_weight("720p_2500k")) self.assertTrue( stream_weight("720p60_3000k") > stream_weight("720p_3000k")) self.assertTrue( stream_weight("720p_3000k") < stream_weight("720p+_3000k")) self.assertTrue( stream_weight("3000k") > stream_weight("2500k"))