From 52636ae1b7bd18e1cd0934289e2ec52e7be84a3d Mon Sep 17 00:00:00 2001 From: Niall McAndrew Date: Sat, 25 Aug 2012 21:30:11 +1200 Subject: Added GOMTV plugin support adapted from https://github.com/sjp/GOMstreamer --- src/livestreamer/cli.py | 16 +++ src/livestreamer/compat.py | 3 +- src/livestreamer/plugins/__init__.py | 4 +- src/livestreamer/plugins/gomtv.py | 202 +++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 src/livestreamer/plugins/gomtv.py diff --git a/src/livestreamer/cli.py b/src/livestreamer/cli.py index e04f545..396b8b2 100644 --- a/src/livestreamer/cli.py +++ b/src/livestreamer/cli.py @@ -2,6 +2,7 @@ import argparse import os import sys import subprocess +import getpass from livestreamer import * from livestreamer.compat import input, stdout, is_win32 @@ -64,6 +65,13 @@ pluginopt.add_argument("-r", "--rtmpdump", metavar="path", help="Specify location of rtmpdump") pluginopt.add_argument("-j", "--jtv-cookie", metavar="cookie", help="Specify JustinTV cookie to allow access to subscription channels") +pluginopt.add_argument("--gomtv-cookie", metavar="cookie", + help="Specify GOMTV cookie to allow access to streams") +pluginopt.add_argument("--gomtv-username", metavar="username", + help="Specify GOMTV username to allow access to streams") +pluginopt.add_argument("--gomtv-password", metavar="password", + help="Specify GOMTV password to allow access to streams (If left blank you will be prompted)", + nargs="?", const=True, default=None) if is_win32: RCFILE = os.path.join(os.environ["APPDATA"], "livestreamer", "livestreamerrc") @@ -257,9 +265,17 @@ def main(): if args.stdout or args.output == "-": set_msg_output(sys.stderr) + if args.gomtv_password is True: + gomtv_password = getpass.getpass("GOMTV Password:") + else: + gomtv_password = args.gomtv_password + livestreamer.set_option("errorlog", args.errorlog) livestreamer.set_option("rtmpdump", args.rtmpdump) livestreamer.set_plugin_option("justintv", "cookie", args.jtv_cookie) + livestreamer.set_plugin_option("gomtv", "cookie", args.gomtv_cookie) + livestreamer.set_plugin_option("gomtv", "username", args.gomtv_username) + livestreamer.set_plugin_option("gomtv", "password", gomtv_password) livestreamer.set_loglevel(args.loglevel) if args.url: diff --git a/src/livestreamer/compat.py b/src/livestreamer/compat.py index 5bc1c3c..61711fb 100644 --- a/src/livestreamer/compat.py +++ b/src/livestreamer/compat.py @@ -24,9 +24,10 @@ except ImportError: import urllib2 as urllib try: - from urllib.parse import urlparse, parse_qs + from urllib.parse import urlparse, parse_qs, urlencode except ImportError: from urlparse import urlparse, parse_qs + from urllib import urlencode __all__ = ["is_py2", "is_py3", "is_win32", "input", "stdout", "str", "bytes", "urllib", "urlparse", "parse_qs"] diff --git a/src/livestreamer/plugins/__init__.py b/src/livestreamer/plugins/__init__.py index ad96f6c..b0cdbb5 100644 --- a/src/livestreamer/plugins/__init__.py +++ b/src/livestreamer/plugins/__init__.py @@ -34,8 +34,8 @@ class Plugin(object): to be of highest quality. """ - ranking = ["iphonelow", "iphonehigh", "240p", "320k", "360p", "850k", - "480p", "1400k", "720p", "2400k", "hd", "1080p", "live"] + ranking = ["iphonelow", "iphonehigh", "240p", "320k", "360p", "SQTest", "SQ", "850k", + "480p", "HQTest", "HQ", "1400k", "720p", "2400k", "hd", "1080p", "live"] streams = self._get_streams() for rank in reversed(ranking): if rank in streams: diff --git a/src/livestreamer/plugins/gomtv.py b/src/livestreamer/plugins/gomtv.py new file mode 100644 index 0000000..b2aa494 --- /dev/null +++ b/src/livestreamer/plugins/gomtv.py @@ -0,0 +1,202 @@ +''' +This is derived from https://github.com/sjp/GOMstreamer + +and carries the following licence + +Copyright 2010 Simon Potter, Tomas Herman +Copyright 2011 Simon Potter +Copyright 2011 Fj (fj.mail@gmail.com) +Copyright 2012 Niall McAndrew (niallm90@gmail.com) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +''' +from livestreamer.compat import str, bytes, urlencode +from livestreamer.plugins import Plugin, PluginError, NoStreamsError +from livestreamer.stream import HTTPStream +from livestreamer.utils import urlget, urllib +from livestreamer.options import Options + +import xml.dom.minidom, re +import cookielib +import Cookie + +class GomTV(Plugin): + options = Options({ + "cookie": None, + "username": None, + "password": None, + }) + + @classmethod + def can_handle_url(self, url): + return "gomtv.net" in url + + + def _get_streams(self): + options = self.options + # Setting urllib up so that we can store cookies + self.cookiejar = cookielib.LWPCookieJar() + self.opener = urllib.build_opener(urllib.HTTPCookieProcessor(self.cookiejar)) + + if options.get("cookie"): + self.authenticate(cookie=options.get("cookie")) + else: + self.authenticate(options.get("username"), options.get("password")) + + streams = {} + qualities = ["HQ", "SQ", "HQTest", "SQTest"] + streamChoice = "both" + + response = self.grabLivePage(self.url) + + goxUrls = [] + validGoxFound = False + failedGoxAll = False + for quality in qualities: + urls = self.parseHTML(response, quality) + + for url in urls: + # Grab the response of the URL listed on the Live page for a stream + goxFile = urlget(url, opener=self.opener) + + # The response for the GOX XML if an incorrect stream quality is chosen is 1002. + if (goxFile != '1002' and goxFile != ''): + streamUrl = self.parseStreamURL(goxFile) + req = urllib.Request(streamUrl, headers={"User-Agent": "KPeerClient"}) + streams[quality] = HTTPStream(self.session, req) + validGoxFound = True + + return streams + + def authenticate(self, username=None, password=None, cookie=None): + if (username is None or password is None) and cookie is None: + raise PluginError("GOMTV.net Requires a username and password or cookie") + + + if cookie is not None: + name,value = cookie.split("=") + + c = cookielib.Cookie(version=0, name=name, value=value, + port=None, port_specified=False, domain='gomtv.net', + domain_specified=False, domain_initial_dot=False, path='/', + path_specified=True, secure=False, expires=None, discard=True, + comment=None, comment_url=None, rest={'HttpOnly': None}, + rfc2109=False) + self.cookiejar.set_cookie(c) + else: + values = { + 'cmd': 'login', + 'rememberme': '1', + 'mb_username': username, + 'mb_password': password + } + data = urlencode(values) + # Now expects to log in only via the website. Thanks chrippa. + headers = {'Referer': 'http://www.gomtv.net/'} + request = urllib.Request('https://ssl.gomtv.net/userinfo/loginProcess.gom', data, headers) + urlget(request, opener=self.opener) + + req = urllib.Request('http://www.gomtv.net/forum/list.gom?m=my') + if 'Please need login' in urlget(req, opener=self.opener): + raise PluginError("Authentication failed") + + # The real response that we want are the cookies, so returning None is fine. + return + + def getEventLivePageURL(self, gomtvLiveURL, response): + match = re.search(' \"(.*)\";', response) + assert match, 'Event Live Page URL not found' + return urljoin(gomtvLiveURL, match.group(1)) + + def getSeasonURL(self, gomtvURL): + # Getting season url from the 'Go Live!' button on the main page. + match = re.search('.*liveicon">\d+)\"\sclass=\"live_now\"\stitle=\"(?P[^\"]+)' + live_streams = re.findall(patternLive, response) + + if len(live_streams) > 1: + liveUrls = [] + options = range(len(live_streams)) + for i in options: + # Modify the urlFromHTML according to the user + singleUrlFromHTML = re.sub(r'conid=\d+', 'conid=' + live_streams[i][0], urlFromHTML) + singleTitleHTML = '+'.join(live_streams[i][1].split(' ')) + singleUrlFromHTML = re.sub(r'title=[\w|.|+]*', 'title=' + singleTitleHTML, singleUrlFromHTML) + liveUrls.append(singleUrlFromHTML) + return liveUrls + else: + if urlFromHTML is None: + return [] + else: + return [urlFromHTML] + + def parseStreamURL(self, response): + # Grabbing the gomcmd URL + try: + streamPattern = r'<REF href="([^"]*)"\s*/>' + regexResult = re.search(streamPattern, response).group(1) + except AttributeError: + return None + + regexResult = urllib.unquote(regexResult) + regexResult = re.sub(r'&', '&', regexResult) + # SQ and SQTest streams can be gomp2p links, with actual stream address passed as a parameter. + if regexResult.startswith('gomp2p://'): + regexResult, n = re.subn(r'^.*LiveAddr=', '', regexResult) + # Cosmetics, getting rid of the HTML entity, we don't + # need either of the " character or " + regexResult = regexResult.replace('"', '') + return regexResult + +__plugin__ = GomTV -- cgit v1.2.3 From 550257a2a0b3c2fa4e427cb5a0a6c75e4b727179 Mon Sep 17 00:00:00 2001 From: Niall McAndrew <niallm90@gmail.com> Date: Sat, 25 Aug 2012 21:40:04 +1200 Subject: Added reference to GOMTV.net licence and added it to the readme plugin list. --- LICENSE | 4 +++- README.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c7fa085..e0ec21f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,6 @@ +The GOMTV.net plugin is under a seperate licence. Please see the gomtv.py file +for details. + Copyright (c) 2011-2012, Christopher Rosell All rights reserved. @@ -20,4 +23,3 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/README.md b/README.md index f669f97..34d85c8 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ streaming services in a custom video player. Currently supported sites are: +* GOMTV.net * Justin.tv/Twitch.tv * Own3d.tv * SVTPlay -- cgit v1.2.3