Repository: streamlink/streamlink

Issue:
Nimo tv
<!--
Thanks for requesting a plugin!
USE THE TEMPLATE. Otherwise your plugin request may be rejected.

First, see the contribution guidelines and plugin request requirements:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Plugin requests which fall into the categories we will not implement will be closed immediately.

Also check the list of open and closed plugin requests:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+request%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Plugin Request

<!-- Replace [ ] with [x] in order to check the box -->
- [x] This is a plugin request and I have read the contribution guidelines and plugin request requirements.


### Description

<!-- Explain the plugin and site as clearly as you can. What is the site about? Who runs it? What content does it provide? What value does it bring to Streamlink? Etc. -->


### Example stream URLs

<!-- Example URLs for streams are required. Plugin requests which do not have example URLs will be closed. -->

1. http://nimo.tv/
2. https://www.nimo.tv/revo-donkey
3. https://www.nimo.tv/live/106070469


### Additional comments, screenshots, etc.

nimotv is gaming stream app from indonesia, please add this plugin

[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)



Discussion:

Resolving patch:
diff --git a/src/streamlink/plugins/nimotv.py b/src/streamlink/plugins/nimotv.py
new file mode 100644
--- /dev/null
+++ b/src/streamlink/plugins/nimotv.py
@@ -0,0 +1,97 @@
+import logging
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import useragents, validate
+from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
+
+log = logging.getLogger(__name__)
+
+
+class NimoTV(Plugin):
+    url_re = re.compile(r'https?://(?:www\.)?nimo\.tv/(?P<username>.*)')
+    data_url = 'https://m.nimo.tv/{0}'
+    data_re = re.compile(r'<script>var G_roomBaseInfo = ({.*?});</script>')
+
+    author = None
+    category = None
+    title = None
+
+    data_schema = validate.Schema(
+        validate.transform(data_re.search),
+        validate.any(None, validate.all(
+            validate.get(1),
+            validate.transform(parse_json), {
+                'title': str,
+                'nickname': str,
+                'game': str,
+                'roomLineInfo': validate.any(None, {
+                    'vCodeLines2': [{
+                        'iBitRate': int,
+                        'vCdns': [{
+                            'vCdnUrls': [{
+                                'smediaUrl': validate.url(),
+                            }],
+                        }],
+                    }],
+                }),
+            },
+        )),
+    )
+
+    video_qualities = {
+        250: '240p',
+        500: '360p',
+        1000: '480p',
+        2000: '720p',
+        6000: '1080p',
+    }
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def get_author(self):
+        if self.author is not None:
+            return self.author
+
+    def get_category(self):
+        if self.category is not None:
+            return self.category
+
+    def get_title(self):
+        if self.title is not None:
+            return self.title
+
+    def _get_streams(self):
+        m = self.url_re.match(self.url)
+        if m and m.group('username'):
+            username = m.group('username')
+        else:
+            return
+
+        headers = {'User-Agent': useragents.ANDROID}
+        data = self.session.http.get(
+            self.data_url.format(username),
+            headers=headers,
+            schema=self.data_schema,
+        )
+
+        if data is None or data['roomLineInfo'] is None:
+            return
+
+        self.author = data['nickname']
+        self.category = data['game']
+        self.title = data['title']
+
+        for vcl2 in data['roomLineInfo']['vCodeLines2']:
+            q = self.video_qualities[vcl2['iBitRate']]
+            for vcdn in vcl2['vCdns']:
+                for vcdnurl in vcdn['vCdnUrls']:
+                    if 'tx.hls.nimo.tv' in vcdnurl['smediaUrl']:
+                        log.debug(f"HLS URL={vcdnurl['smediaUrl']} ({q})")
+                        yield q, HLSStream(self.session, vcdnurl['smediaUrl'])
+
+
+__plugin__ = NimoTV


Test patch:
diff --git a/tests/plugins/test_nimotv.py b/tests/plugins/test_nimotv.py
new file mode 100644
--- /dev/null
+++ b/tests/plugins/test_nimotv.py
@@ -0,0 +1,22 @@
+import unittest
+
+from streamlink.plugins.nimotv import NimoTV
+
+
+class TestPluginNimoTV(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'http://www.nimo.tv/live/737614',
+            'https://www.nimo.tv/live/737614',
+            'http://www.nimo.tv/sanz',
+            'https://www.nimo.tv/sanz',
+        ]
+        for url in should_match:
+            self.assertTrue(NimoTV.can_handle_url(url))
+
+    def test_can_handle_url_negative(self):
+        should_not_match = [
+            'https://example.com/index.html',
+        ]
+        for url in should_not_match:
+            self.assertFalse(NimoTV.can_handle_url(url))