The branch, frodo has been updated
via 105a79625b4885d0e5ef5680026155460e380d76 (commit)
from caaa3935a14c007dd429cab759d9654b41c01f3c (commit)
- Log -----------------------------------------------------------------
http://xbmc.git.sourceforge.net/git/gitweb.cgi?p=xbmc/plugins;a=commit;h=105a79625b4885d0e5ef5680026155460e380d76
commit 105a79625b4885d0e5ef5680026155460e380d76
Author: beenje <[email protected]>
Date: Tue Aug 6 22:31:02 2013 +0200
[plugin.video.twitch] updated to version 1.0.2
diff --git a/plugin.video.twitch/README.md b/plugin.video.twitch/README.md
index 9fb32b3..81ef59a 100644
--- a/plugin.video.twitch/README.md
+++ b/plugin.video.twitch/README.md
@@ -10,18 +10,18 @@ FAQ
> In most cases this is caused by a too old librtmp version. Updating it could
> solve the problem:
+> [Link: librtmp - Help Thread at
xbmc.org](http://forum.xbmc.org/showthread.php?tid=162307
"http://forum.xbmc.org/showthread.php?tid=162307")
+>
> [Link: How to update
> librtmp](http://wiki.xbmc.org/index.php?title=HOW-TO:Update_librtmp
> "http://wiki.xbmc.org/index.php?title=HOW-TO:Update_librtmp")
* I can't find the Twitch.tv add-on in the xbmc add-on manager!
-> Make sure you are using at least XBMC 11 Eden.
+> Make sure you are using at least XBMC 12 Frodo.
What's next?
----------------
Things that need to be done next:
-* Finish code refactoring
-* Test all features of the plug-in
* Implement a user authentication
* Implementation of a section for archived videos
diff --git a/plugin.video.twitch/addon.xml b/plugin.video.twitch/addon.xml
index d909d96..6a4a8d1 100644
--- a/plugin.video.twitch/addon.xml
+++ b/plugin.video.twitch/addon.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
-<addon id='plugin.video.twitch' version='1.0.0' name='TwitchTV'
provider-name='StateOfTheArt'>
+<addon id='plugin.video.twitch' version='1.0.2' name='TwitchTV'
provider-name='StateOfTheArt and ccaspers'>
<requires>
<import addon='xbmc.python' version='2.1.0'/>
<import addon='script.module.simplejson' version='2.0.10'/>
diff --git a/plugin.video.twitch/changelog.txt
b/plugin.video.twitch/changelog.txt
index 6c97b4a..56d68aa 100644
--- a/plugin.video.twitch/changelog.txt
+++ b/plugin.video.twitch/changelog.txt
@@ -43,4 +43,8 @@
- major code refactoring
- extracted twitch api
- new search function
-- changed code style to match pep8 requirements
\ No newline at end of file
+- changed code style to match pep8 requirements
+1.0.1
+- fixed bug: streams with optional subscriptions
+1.0.2
+- fixed stream resolving
diff --git a/plugin.video.twitch/converter.py b/plugin.video.twitch/converter.py
index 19a1eeb..aa6ce51 100644
--- a/plugin.video.twitch/converter.py
+++ b/plugin.video.twitch/converter.py
@@ -1,107 +1,105 @@
from twitch import Keys
+
class JsonListItemConverter(object):
-
+
def __init__(self, PLUGIN, title_length):
self.plugin = PLUGIN
self.titleBuilder = TitleBuilder(PLUGIN, title_length)
-
+
def convertGameToListItem(self, game):
name = game[Keys.NAME].encode('utf-8')
image = game[Keys.LOGO].get(Keys.LARGE, '')
- return {
- 'label': name,
+ return {'label': name,
'path': self.plugin.url_for('createListForGame',
- gameName = name, index = '0'),
- 'icon' : image
+ gameName=name, index='0'),
+ 'icon': image
}
-
+
def convertTeamToListItem(self, team):
name = team['name']
- return {
- 'label': name,
+ return {'label': name,
'path': self.plugin.url_for(endpoint='createListOfTeamStreams',
team=name),
- 'icon' : team.get(Keys.LOGO,'')
+ 'icon': team.get(Keys.LOGO, '')
}
-
- def convertTeamChannelToListItem(self,teamChannel):
- images = teamChannel.get('image','')
- image = '' if not images else images.get('size600','')
-
+
+ def convertTeamChannelToListItem(self, teamChannel):
+ images = teamChannel.get('image', '')
+ image = '' if not images else images.get('size600', '')
+
channelname = teamChannel['name']
- titleValues = {'streamer':teamChannel.get('display_name'),
- 'title':teamChannel.get('title'),
- 'viewers':teamChannel.get('current_viewers')}
-
+ titleValues = {'streamer': teamChannel.get('display_name'),
+ 'title': teamChannel.get('title'),
+ 'viewers': teamChannel.get('current_viewers')}
+
title = self.titleBuilder.formatTitle(titleValues)
return {'label': title,
'path': self.plugin.url_for(endpoint='playLive',
name=channelname),
- 'is_playable' : True,
- 'icon' : image}
-
+ 'is_playable': True,
+ 'icon': image}
+
def extractTitleValues(self, channel):
- return {
- 'streamer': channel.get(Keys.DISPLAY_NAME,
+ return {'streamer': channel.get(Keys.DISPLAY_NAME,
self.plugin.get_string(34000)),
'title': channel.get(Keys.STATUS,
self.plugin.get_string(34001)),
- 'viewers':channel.get(Keys.VIEWERS,
- self.plugin.get_string(34002))
+ 'viewers': channel.get(Keys.VIEWERS,
+ self.plugin.get_string(34002))
}
def convertChannelToListItem(self, channel):
videobanner = channel.get(Keys.VIDEO_BANNER, '')
logo = channel.get(Keys.LOGO, '')
- return {
- 'label': self.getTitleForChannel(channel),
- 'path': self.plugin.url_for(endpoint = 'playLive',
- name = channel[Keys.NAME]),
+ return {'label': self.getTitleForChannel(channel),
+ 'path': self.plugin.url_for(endpoint='playLive',
+ name=channel[Keys.NAME]),
'is_playable': True,
- 'icon' : videobanner if videobanner else logo
+ 'icon': videobanner if videobanner else logo
}
-
+
def getTitleForChannel(self, channel):
titleValues = self.extractTitleValues(channel)
return self.titleBuilder.formatTitle(titleValues)
-
+
+
class TitleBuilder(object):
-
+
class Templates(object):
TITLE = "{title}"
STREAMER = "{streamer}"
STREAMER_TITLE = "{streamer} - {title}"
VIEWERS_STREAMER_TITLE = "{viewers} - {streamer} - {title}"
ELLIPSIS = '...'
-
+
def __init__(self, PLUGIN, line_length):
self.plugin = PLUGIN
self.line_length = line_length
-
+
def formatTitle(self, titleValues):
- titleSetting = int(self.plugin.get_setting('titledisplay'))
+ titleSetting = int(self.plugin.get_setting('titledisplay', unicode))
template = self.getTitleTemplate(titleSetting)
-
+
for key, value in titleValues.iteritems():
titleValues[key] = self.cleanTitleValue(value)
title = template.format(**titleValues)
-
+
return self.truncateTitle(title)
-
+
def getTitleTemplate(self, titleSetting):
- options = {0:TitleBuilder.Templates.STREAMER_TITLE,
- 1:TitleBuilder.Templates.VIEWERS_STREAMER_TITLE,
- 2:TitleBuilder.Templates.TITLE,
- 3:TitleBuilder.Templates.STREAMER}
+ options = {0: TitleBuilder.Templates.STREAMER_TITLE,
+ 1: TitleBuilder.Templates.VIEWERS_STREAMER_TITLE,
+ 2: TitleBuilder.Templates.TITLE,
+ 3: TitleBuilder.Templates.STREAMER}
return options.get(titleSetting, TitleBuilder.Templates.STREAMER)
-
+
def cleanTitleValue(self, value):
if isinstance(value, basestring):
return unicode(value).replace('\r\n', ' ').strip().encode('utf-8')
else:
return value
-
+
def truncateTitle(self, title):
shortTitle = title[:self.line_length]
ending = (title[self.line_length:] and TitleBuilder.Templates.ELLIPSIS)
- return shortTitle + ending
\ No newline at end of file
+ return shortTitle + ending
diff --git a/plugin.video.twitch/default.py b/plugin.video.twitch/default.py
index 69d7352..214225d 100644
--- a/plugin.video.twitch/default.py
+++ b/plugin.video.twitch/default.py
@@ -1,9 +1,9 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
-from converter import JsonListItemConverter
+from converter import JsonListItemConverter
from functools import wraps
from twitch import TwitchTV, TwitchVideoResolver, Keys, TwitchException
-from xbmcswift2 import Plugin #@UnresolvedImport
+from xbmcswift2 import Plugin # @UnresolvedImport
import sys
ITEMS_PER_PAGE = 20
@@ -16,7 +16,7 @@ TWITCHTV = TwitchTV()
def managedTwitchExceptions(func):
@wraps(func)
- def wrapper(*args,**kwargs):
+ def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except TwitchException as error:
@@ -28,7 +28,7 @@ def handleTwitchException(exception):
codeTranslations = {TwitchException.NO_STREAM_URL : 32004,
TwitchException.STREAM_OFFLINE : 32002,
TwitchException.HTTP_ERROR : 32001,
- TwitchException.JSON_ERROR : 32008 }
+ TwitchException.JSON_ERROR : 32008}
code = exception.code
title = 31000
msg = codeTranslations[code]
@@ -38,29 +38,23 @@ def handleTwitchException(exception):
@PLUGIN.route('/')
def createMainListing():
items = [
- {
- 'label': PLUGIN.get_string(30005),
- 'path': PLUGIN.url_for(endpoint = 'createListOfFeaturedStreams')
+ {'label': PLUGIN.get_string(30005),
+ 'path': PLUGIN.url_for(endpoint='createListOfFeaturedStreams')
},
- {
- 'label': PLUGIN.get_string(30001),
- 'path': PLUGIN.url_for(endpoint = 'createListOfGames', index = '0')
+ {'label': PLUGIN.get_string(30001),
+ 'path': PLUGIN.url_for(endpoint='createListOfGames', index='0')
},
- {
- 'label': PLUGIN.get_string(30002),
- 'path': PLUGIN.url_for(endpoint = 'createFollowingList')
+ {'label': PLUGIN.get_string(30002),
+ 'path': PLUGIN.url_for(endpoint='createFollowingList')
},
- {
- 'label': PLUGIN.get_string(30006),
- 'path': PLUGIN.url_for(endpoint = 'createListOfTeams')
+ {'label': PLUGIN.get_string(30006),
+ 'path': PLUGIN.url_for(endpoint='createListOfTeams')
},
- {
- 'label': PLUGIN.get_string(30003),
- 'path': PLUGIN.url_for(endpoint = 'search')
+ {'label': PLUGIN.get_string(30003),
+ 'path': PLUGIN.url_for(endpoint='search')
},
- {
- 'label': PLUGIN.get_string(30004),
- 'path': PLUGIN.url_for(endpoint = 'showSettings')
+ {'label': PLUGIN.get_string(30004),
+ 'path': PLUGIN.url_for(endpoint='showSettings')
}
]
return items
@@ -93,7 +87,7 @@ def createListForGame(gameName, index):
items = [CONVERTER.convertChannelToListItem(item[Keys.CHANNEL])for item
in TWITCHTV.getGameStreams(gameName, offset, limit)]
- items.append(linkToNextPage('createListForGame', index, gameName =
gameName))
+ items.append(linkToNextPage('createListForGame', index, gameName=gameName))
return items
@@ -110,20 +104,20 @@ def createFollowingList():
def search():
query = PLUGIN.keyboard('', PLUGIN.get_string(30101))
if query:
- target = PLUGIN.url_for(endpoint = 'searchresults', query = query,
index = '0')
+ target = PLUGIN.url_for(endpoint='searchresults', query=query,
index='0')
else:
- target = PLUGIN.url_for(endpoint = 'createMainListing')
+ target = PLUGIN.url_for(endpoint='createMainListing')
PLUGIN.redirect(target)
@PLUGIN.route('/searchresults/<query>/<index>/')
@managedTwitchExceptions
-def searchresults(query, index = '0'):
+def searchresults(query, index='0'):
index, offset, limit = calculatePaginationValues(index)
streams = TWITCHTV.searchStreams(query, offset, limit)
-
+
items = [CONVERTER.convertChannelToListItem(stream[Keys.CHANNEL]) for
stream in streams]
- items.append(linkToNextPage('searchresults', index, query = query))
+ items.append(linkToNextPage('searchresults', index, query=query))
return items
@@ -152,7 +146,7 @@ def createListOfTeams():
@PLUGIN.route('/createListOfTeamStreams/<team>/')
@managedTwitchExceptions
def createListOfTeamStreams(team):
- return [CONVERTER.convertTeamChannelToListItem(channel[Keys.CHANNEL])
+ return [CONVERTER.convertTeamChannelToListItem(channel[Keys.CHANNEL])
for channel in TWITCHTV.getTeamStreams(team)]
@@ -164,23 +158,22 @@ def calculatePaginationValues(index):
def getUserName():
- username = PLUGIN.get_setting('username').lower()
+ username = PLUGIN.get_setting('username', unicode).lower()
if not username:
PLUGIN.open_settings()
- username = PLUGIN.get_setting('username').lower()
+ username = PLUGIN.get_setting('username', unicode).lower()
return username
def getVideoQuality():
- chosenQuality = PLUGIN.get_setting('video')
- qualities = {'0':sys.maxint, '1':720, '2':480, '3':360}
+ chosenQuality = PLUGIN.get_setting('video', unicode)
+ qualities = {'0': sys.maxint, '1': 720, '2': 480, '3': 360}
return qualities.get(chosenQuality, sys.maxint)
def linkToNextPage(target, currentIndex, **kwargs):
- return {
- 'label': PLUGIN.get_string(31001),
- 'path': PLUGIN.url_for(target, index = str(currentIndex+1),
**kwargs)
+ return {'label': PLUGIN.get_string(31001),
+ 'path': PLUGIN.url_for(target, index=str(currentIndex + 1),
**kwargs)
}
if __name__ == '__main__':
diff --git a/plugin.video.twitch/twitch.py b/plugin.video.twitch/twitch.py
index 7530a2b..16b4fc8 100644
--- a/plugin.video.twitch/twitch.py
+++ b/plugin.video.twitch/twitch.py
@@ -9,11 +9,12 @@ except:
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101
Firefox/6.0'
+
class JSONScraper(object):
'''
Encapsulates execution request and parsing of response
'''
- def _downloadWebData(self, url, headers = None):
+ def _downloadWebData(self, url, headers=None):
req = urllib2.Request(url)
req.add_header(Keys.USER_AGENT, USER_AGENT)
response = urllib2.urlopen(req)
@@ -21,7 +22,7 @@ class JSONScraper(object):
response.close()
return data
- def getJson(self, url, headers = None):
+ def getJson(self, url, headers=None):
try:
jsonString = self._downloadWebData(url, headers)
except:
@@ -35,7 +36,7 @@ class JSONScraper(object):
class TwitchTV(object):
'''
Uses Twitch API to fetch json-encoded objects
- every method returns a dict containing the objects' values
+ every method returns a dict containing the objects\' values
'''
def __init__(self):
self.scraper = JSONScraper()
@@ -44,23 +45,23 @@ class TwitchTV(object):
url = ''.join([Urls.STREAMS, Keys.FEATURED])
return self._fetchItems(url, Keys.FEATURED)
- def getGames(self, offset = 10, limit = 10):
+ def getGames(self, offset=10, limit=10):
options = Urls.OPTIONS_OFFSET_LIMIT.format(offset, limit)
url = ''.join([Urls.GAMES, Keys.TOP, options])
return self._fetchItems(url, Keys.TOP)
- def getGameStreams(self, gameName, offset = 10, limit = 10):
+ def getGameStreams(self, gameName, offset=10, limit=10):
quotedName = quote_plus(gameName)
options = Urls.OPTIONS_OFFSET_LIMIT_GAME.format(offset, limit,
quotedName)
url = ''.join([Urls.BASE, Keys.STREAMS, options])
return self._fetchItems(url, Keys.STREAMS)
- def searchStreams(self, query, offset = 10, limit = 10):
+ def searchStreams(self, query, offset=10, limit=10):
quotedQuery = quote_plus(query)
options = Urls.OPTIONS_OFFSET_LIMIT_QUERY.format(offset, limit,
quotedQuery)
url = ''.join([Urls.SEARCH, Keys.STREAMS, options])
return self._fetchItems(url, Keys.STREAMS)
-
+
def getFollowingStreams(self, username):
#Get ChannelNames
followingChannels = self.getFollowingChannelNames(username)
@@ -69,28 +70,27 @@ class TwitchTV(object):
options = '?channel=' + ','.join(channelNames)
url = ''.join([Urls.BASE, Keys.STREAMS, options])
return self._fetchItems(url, Keys.STREAMS)
-
+
def getFollowingChannelNames(self, username):
quotedUsername = quote_plus(username)
url = Urls.FOLLOWED_CHANNELS.format(quotedUsername)
return self._fetchItems(url, Keys.FOLLOWS)
-
+
def getTeams(self):
return self._fetchItems(Urls.TEAMS, Keys.TEAMS)
-
+
def getTeamStreams(self, teamName):
'''
- Consider this method to be unstable, because the
+ Consider this method to be unstable, because the
requested resource is not part of the official Twitch API
'''
quotedTeamName = quote_plus(teamName)
url = Urls.TEAMSTREAM.format(quotedTeamName)
return self._fetchItems(url, Keys.CHANNELS)
-
-
+
def _filterChannelNames(self, channels):
return [item[Keys.CHANNEL][Keys.NAME] for item in channels]
-
+
def _fetchItems(self, url, key):
items = self.scraper.getJson(url)
return items[key] if items else []
@@ -101,24 +101,22 @@ class TwitchVideoResolver(object):
Resolves the RTMP-Link to a given Channelname
Uses Justin.TV API
'''
-
+
def getRTMPUrl(self, channelName, maxQuality):
swfUrl = self._getSwfUrl(channelName)
streamQualities = self._getStreamsForChannel(channelName)
# check that api response isn't empty (i.e. stream is offline)
- if streamQualities:
- items = [
- self._parseStreamValues(stream, swfUrl)
+ if streamQualities:
+ items = [self._parseStreamValues(stream, swfUrl)
for stream in streamQualities
- if self._streamIsAccessible(stream)
- ]
+ if self._streamIsAccessible(stream)]
if items:
return self._bestMatchForChosenQuality(items,
maxQuality)[Keys.RTMP_URL]
else:
raise TwitchException(TwitchException.NO_STREAM_URL)
else:
raise TwitchException(TwitchException.STREAM_OFFLINE)
-
+
def _getSwfUrl(self, channelName):
url = Urls.TWITCH_SWF + channelName
headers = {Keys.USER_AGENT: USER_AGENT,
@@ -128,17 +126,21 @@ class TwitchVideoResolver(object):
return response.geturl()
def _streamIsAccessible(self, stream):
- if not stream[Keys.TOKEN] and re.match(Patterns.IP,
stream.get(Keys.CONNECT)):
+ stream_is_public = (stream.get(Keys.NEEDED_INFO) !=
"channel_subscription")
+ stream_has_token = stream.get(Keys.TOKEN)
+
+ if stream.get(Keys.CONNECT) is None:
return False
- return True
+
+ return stream_is_public and stream_has_token
def _getStreamsForChannel(self, channelName):
scraper = JSONScraper()
- url = Urls.TWITCH_API.format(channel = channelName)
+ url = Urls.TWITCH_API.format(channel=channelName)
return scraper.getJson(url)
def _parseStreamValues(self, stream, swfUrl):
- streamVars = {Keys.SWF_URL : swfUrl}
+ streamVars = {Keys.SWF_URL: swfUrl}
streamVars[Keys.RTMP] = stream[Keys.CONNECT]
streamVars[Keys.PLAYPATH] = stream.get(Keys.PLAY)
@@ -153,17 +155,20 @@ class TwitchVideoResolver(object):
Keys.RTMP_URL: Urls.FORMAT_FOR_RTMP.format(**streamVars)}
def _bestMatchForChosenQuality(self, streams, maxQuality):
- streams = [stream for stream in streams
- if stream[Keys.QUALITY] <= maxQuality]
- streams.sort(key=lambda t: t[Keys.QUALITY], reverse=True)
- return streams[0]
+ streams.sort(key=lambda t: t[Keys.QUALITY])
+ bestMatch = streams[0]
+ for stream in streams:
+ if stream[Keys.QUALITY] <= maxQuality:
+ bestMatch = stream
+ return bestMatch
class Keys(object):
'''
- Should not be instantiated, just used to categorize
+ Should not be instantiated, just used to categorize
string-constants
'''
+
CHANNEL = 'channel'
CHANNELS = 'channels'
CONNECT = 'connect'
@@ -175,6 +180,7 @@ class Keys(object):
LOGO = 'logo'
LARGE = 'large'
NAME = 'name'
+ NEEDED_INFO = 'needed_info'
PLAY = 'play'
PLAYPATH = 'playpath'
QUALITY = 'quality'
@@ -189,53 +195,56 @@ class Keys(object):
TOKEN = 'token'
TOP = 'top'
USER_AGENT = 'User-Agent'
- VIDEO_BANNER = 'video_banner'
+ VIDEO_BANNER = 'video_banner'
VIDEO_HEIGHT = 'video_height'
VIEWERS = 'viewers'
+
class Patterns(object):
'''
- Should not be instantiated, just used to categorize
+ Should not be instantiated, just used to categorize
string-constants
'''
VALID_FEED =
"^https?:\/\/(?:[^\.]*.)?(?:twitch|justin)\.tv\/([a-zA-Z0-9_]+).*$"
IP = '.*\d+\.\d+\.\d+\.\d+.*'
EXPIRATION = '.*"expiration": (\d+)[^\d].*'
+
class Urls(object):
'''
- Should not be instantiated, just used to categorize
+ Should not be instantiated, just used to categorize
string-constants
'''
TWITCH_TV = 'http://www.twitch.tv/'
BASE = 'https://api.twitch.tv/kraken/'
- FOLLOWED_CHANNELS = BASE + 'users/{0}/follows/channels'
+ FOLLOWED_CHANNELS = BASE + 'users/{0}/follows/channels?limit=100'
GAMES = BASE + 'games/'
STREAMS = BASE + 'streams/'
SEARCH = BASE + 'search/'
TEAMS = BASE + 'teams'
-
+
TEAMSTREAM = 'http://api.twitch.tv/api/team/{0}/live_channels.json'
-
+
OPTIONS_OFFSET_LIMIT = '?offset={0}&limit={1}'
OPTIONS_OFFSET_LIMIT_GAME = OPTIONS_OFFSET_LIMIT + '&game={2}'
OPTIONS_OFFSET_LIMIT_QUERY = OPTIONS_OFFSET_LIMIT + '&q={2}'
TWITCH_API =
"http://usher.justin.tv/find/{channel}.json?type=any&group=&channel_subscription="
TWITCH_SWF = "http://www.justin.tv/widgets/live_embed_player.swf?channel="
- FORMAT_FOR_RTMP = "{rtmp}/{playpath} swfUrl={swfUrl} swfVfy=1 {token}
live=1" #Pageurl missing here
-
+ FORMAT_FOR_RTMP = "{rtmp}/{playpath} swfUrl={swfUrl} swfVfy=1 {token}
live=1" # Pageurl missing here
+
+
class TwitchException(Exception):
NO_STREAM_URL = 0
STREAM_OFFLINE = 1
HTTP_ERROR = 2
JSON_ERROR = 3
-
+
def __init__(self, code):
Exception.__init__(self)
self.code = code
-
+
def __str__(self):
return repr(self.code)
-----------------------------------------------------------------------
Summary of changes:
plugin.video.twitch/README.md | 6 +-
plugin.video.twitch/addon.xml | 2 +-
plugin.video.twitch/changelog.txt | 6 ++-
plugin.video.twitch/converter.py | 92 ++++++++++++++++++-------------------
plugin.video.twitch/default.py | 65 ++++++++++++--------------
plugin.video.twitch/twitch.py | 89 +++++++++++++++++++----------------
6 files changed, 132 insertions(+), 128 deletions(-)
hooks/post-receive
--
Plugins
------------------------------------------------------------------------------
Get 100% visibility into Java/.NET code with AppDynamics Lite!
It's a free troubleshooting tool designed for production.
Get down to code-level detail for bottlenecks, with <2% overhead.
Download for free and get started troubleshooting in minutes.
http://pubads.g.doubleclick.net/gampad/clk?id=48897031&iu=/4140/ostg.clktrk
_______________________________________________
Xbmc-addons mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/xbmc-addons