aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Christopher Rosell <chrippa@tanuki.se>2012-08-25 04:03:37 -0700
committerGravatar Christopher Rosell <chrippa@tanuki.se>2012-08-25 04:03:37 -0700
commit91363e5628dd30e34ab254c619246a298333f8bc (patch)
tree2f2d8f74a194a4c5012a3254b58404c79f7e18a5
parent8545ee9aec203c0fcbeb64810022a3018e1590d4 (diff)
parent550257a2a0b3c2fa4e427cb5a0a6c75e4b727179 (diff)
Merge pull request #21 from niallm90/plugin-gomtv
GOMTV.net plugin
-rw-r--r--LICENSE4
-rw-r--r--README.md1
-rw-r--r--src/livestreamer/cli.py16
-rw-r--r--src/livestreamer/compat.py3
-rw-r--r--src/livestreamer/plugins/__init__.py4
-rw-r--r--src/livestreamer/plugins/gomtv.py202
6 files changed, 226 insertions, 4 deletions
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
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"><a href="([^"]*)"', urlget(gomtvURL, opener=self.opener))
+ assert match, 'golive_btn href not found'
+ return match.group(1)
+
+ def grabLivePage(self, gomtvLiveURL):
+ response = urlget(gomtvLiveURL, opener=self.opener)
+ # If a special event occurs, we know that the live page response
+ # will just be some JavaScript that redirects the browser to the
+ # real live page. We assume that the entireity of this JavaScript
+ # is less than 200 characters long, and that real live pages are
+ # more than that.
+ if len(response) < 200:
+ # Grabbing the real live page URL
+ gomtvLiveURL = self.getEventLivePageURL(gomtvLiveURL, response)
+ response = urlget(gomtvLiveURL, opener=self.opener)
+ return response
+
+ def parseHTML(self, response, quality):
+ urlFromHTML = None
+ # Parsing through the live page for a link to the gox XML file.
+ # Quality is simply passed as a URL parameter e.g. HQ, SQ, SQTest
+ try:
+ patternHTML = r'[^/]+var.+(http://www.gomtv.net/gox[^;]+;)'
+ urlFromHTML = re.search(patternHTML, response).group(1)
+ urlFromHTML = re.sub(r'\" \+ playType \+ \"', quality, urlFromHTML)
+ except AttributeError:
+ pass
+
+ # Finding the title of the stream, probably not necessary but
+ # done for completeness
+ try:
+ patternTitle = r'this\.title[^;]+;'
+ titleFromHTML = re.search(patternTitle, response).group(0)
+ titleFromHTML = re.search(r'\"(.*)\"', titleFromHTML).group(0)
+ titleFromHTML = re.sub(r'"', '', titleFromHTML)
+ urlFromHTML = re.sub(r'"\+ tmpThis.title[^;]+;', titleFromHTML, urlFromHTML)
+ except AttributeError:
+ pass
+
+ # Check for multiple streams going at the same time, and extract the conid and the title
+ # Those streams have the class "live_now"
+ patternLive = r'<a\shref=\"/live/index.gom\?conid=(?P<conid>\d+)\"\sclass=\"live_now\"\stitle=\"(?P<title>[^\"]+)'
+ 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'&amp;', '&', 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 &quot;
+ regexResult = regexResult.replace('&quot;', '')
+ return regexResult
+
+__plugin__ = GomTV