The branch, dharma has been updated
via b5f028b74dc84ae21f4fe0a3a8f244cd90bcdab2 (commit)
from c4e8dd19fdcb490521ec440af3dcb90a96f68abd (commit)
- Log -----------------------------------------------------------------
http://xbmc.git.sourceforge.net/git/gitweb.cgi?p=xbmc/plugins;a=commit;h=b5f028b74dc84ae21f4fe0a3a8f244cd90bcdab2
commit b5f028b74dc84ae21f4fe0a3a8f244cd90bcdab2
Author: spiff <[email protected]>
Date: Sun Jan 2 18:36:35 2011 +0100
[plugin.audio.icecast] updated to version 0.0.8
diff --git a/plugin.audio.icecast/README.txt b/plugin.audio.icecast/README.txt
index cc934ce..2130e42 100644
--- a/plugin.audio.icecast/README.txt
+++ b/plugin.audio.icecast/README.txt
@@ -2,24 +2,26 @@ GENERAL
This is a simple XBMC add-on to listen to IceCast online radio stations.
-It is mostly based on the original SHOUTcast add-on. Some additional reserch
and coding by <[email protected]>
+It is based on the original SHOUTcast add-on. Icecast-specific reserch and
coding as well as SQLite support by <[email protected]>
INSTALL
-Move the "plugin.audio.icecast" directory into your "addons" directory.
Restart XBMC.
+The add-on is available in the official XBMC repository.
+If you still want to install manually, move the "plugin.audio.icecast"
directory into your "addons" directory.
-NOTES
+
+TECHNICAL NOTES
1. A note on genres: with IceCast, each server lists one or more words as its
"genre". Two approaches are possible:
- Treat each string as a whole category name; I don't like this, because "pop
rock" and "rock pop" will appear as two different genres
- Split each string into words and use each word as a separate category; this
way, "pop" and "rock" will appear only once, but most stations will apear in
more than one genre. This
is the current behaviour.
-2. To speed up processing and decrease network load (the full IceCast XML is
over 3 MB), the add-on sets up a local cache file. When you first run the
add-on, it gets the XML from the IceCast server; it is then cached and reused
until you quit the add-on.
+2. To speed up processing and decrease network load (the full IceCast XML is
over 3 MB with around 10,000 streams), the add-on sets up a local cache. If
SQLite is available (as with standard Ubuntu release), it will be used since it
is faster. If SQLite is not available, a text file will be used instead; this
is slower, but still better than getting the XML off the Internet every time.
The cache is updated if it is more than 1 day old.
-3. Some IceCast radio stations obviously feed broken UTF-8 in their names and
genres - there's nothing to do about it, complain to the radio station.
+3. Some IceCast radio stations obviously feed broken UTF-8 in their names and
genres - there's nothing to do about it, complain to the radio station. More on
the investigation of the issue here:
http://bilbo.online.bg/~assen/icecast-addon/unicode.htm
4. The client-side search (as server-side seems unavailable with IceCast)
searches in both genres and server names
diff --git a/plugin.audio.icecast/addon.xml b/plugin.audio.icecast/addon.xml
index d9a3966..4c80067 100644
--- a/plugin.audio.icecast/addon.xml
+++ b/plugin.audio.icecast/addon.xml
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.audio.icecast"
name="Icecast"
- version="0.0.5"
+ version="0.0.8"
provider-name="Assen Totin">
<requires>
<import addon="xbmc.python" version="1.0"/>
+ <import addon="script.module.pysqlite" version="2.5.6"/>
</requires>
<extension point="xbmc.python.pluginsource"
library="default.py">
diff --git a/plugin.audio.icecast/default.py b/plugin.audio.icecast/default.py
old mode 100644
new mode 100755
index 6970b39..33ea11b
--- a/plugin.audio.icecast/default.py
+++ b/plugin.audio.icecast/default.py
@@ -1,3 +1,4 @@
+#/*
# * This Program is free software; you can redistribute it and/or modify
# * it under the terms of the GNU General Public License as published by
# * the Free Software Foundation; either version 2, or (at your option)
@@ -15,11 +16,12 @@
# *
# */
-import os, urllib2, string, re
import xbmc, xbmcgui, xbmcplugin, xbmcaddon
+import os, urllib2, string, re, htmlentitydefs, time, unicodedata
+
+from xml.sax.saxutils import unescape
from xml.dom import minidom
from urllib import quote_plus
-import unicodedata
__XBMC_Revision__ = xbmc.getInfoLabel('System.BuildVersion')
__settings__ = xbmcaddon.Addon(id='plugin.audio.icecast')
@@ -34,7 +36,28 @@ __credits__ = "Team XBMC"
BASE_URL = 'http://dir.xiph.org/yp.xml'
CACHE_FILE_NAME = 'icecast.cache'
-
+TIMESTAMP_FILE_NAME = 'icecast.timestamp'
+TIMESTAMP_THRESHOLD = 86400
+
+DB_FILE_NAME = 'icecasl.sqlite'
+DB_CREATE_TABLE_STATIONS = 'CREATE TABLE stations (server_name VARCHAR(255),
listen_url VARCHAR(255), bitrate VARCHAR(255), genre VARCHAR(255));'
+DB_CREATE_TABLE_UPDATES = 'CREATE TABLE updates (unix_timestamp VARCHAR(255));'
+
+# Init function for SQLite
+def initSQLite():
+ sqlite_file_name = getSQLiteFileName()
+ sqlite_con = sqlite.connect(sqlite_file_name)
+ sqlite_cur = sqlite_con.cursor()
+ try:
+ sqlite_cur.execute(DB_CREATE_TABLE_STATIONS)
+ sqlite_cur.execute(DB_CREATE_TABLE_UPDATES)
+ putTimestampSQLite(sqlite_con, sqlite_cur)
+ sqlite_is_empty = 1
+ except:
+ sqlite_is_empty = 0
+ return sqlite_con, sqlite_cur, sqlite_is_empty
+
+# Parse XML line
def getText(nodelist):
rc = []
for node in nodelist:
@@ -42,13 +65,31 @@ def getText(nodelist):
rc.append(node.data)
return ''.join(rc)
-def getCacheFileName():
+# Obtain the full path of "userdata/add_ons" directory
+def getUserdataDir():
path = xbmc.translatePath(__settings__.getAddonInfo('profile'))
if not os.path.exists(path):
os.makedirs(path)
- cache_file_name = os.path.join(path,CACHE_FILE_NAME)
+ return path
+
+# Compose the cache file name
+def getCacheFileName():
+ cache_file_dir = getUserdataDir()
+ cache_file_name = os.path.join(cache_file_dir,CACHE_FILE_NAME)
return cache_file_name
+# Compose the timestamp file name
+def getTimestampFileName():
+ cache_file_dir = getUserdataDir()
+ timestamp_file_name = os.path.join(cache_file_dir,TIMESTAMP_FILE_NAME)
+ return timestamp_file_name
+
+# Compose the SQLite database file anme
+def getSQLiteFileName():
+ cache_file_dir = getUserdataDir()
+ db_file_name = os.path.join(cache_file_dir,DB_FILE_NAME)
+ return db_file_name
+
# Read the XML list from IceCast server
def readRemoteXML():
req = urllib2.Request(BASE_URL)
@@ -57,7 +98,7 @@ def readRemoteXML():
response.close()
return xml
-# Parse XML
+# Parse XML to DOM
def parseXML(xml):
dom = minidom.parseString(xml)
return dom
@@ -77,8 +118,41 @@ def writeLocalXML(xml):
f.write(xml)
f.close()
-# Build the list of genres
-def buildGenreList(dom):
+# Populate SQLite table
+def DOMtoSQLite(dom, sqlite_con, sqlite_cur):
+ sqlite_cur.execute("DELETE FROM stations")
+ sqlite_con.commit()
+
+ entries = dom.getElementsByTagName("entry")
+ for entry in entries:
+
+ listen_url_objects = entry.getElementsByTagName("listen_url")
+ for listen_url_object in listen_url_objects:
+ listen_url = getText(listen_url_object.childNodes)
+ listen_url = re.sub("'","&apos",listen_url)
+
+ server_name_objects = entry.getElementsByTagName("server_name")
+ for server_name_object in server_name_objects:
+ server_name = getText(server_name_object.childNodes)
+ server_name = re.sub("'","&apos",server_name)
+
+ bitrate_objects = entry.getElementsByTagName("bitrate")
+ for bitrate_object in bitrate_objects:
+ bitrate = getText(bitrate_object.childNodes)
+
+ genre_objects = entry.getElementsByTagName("genre")
+ for genre_object in genre_objects:
+ genre_name = getText(genre_object.childNodes)
+
+ for genre_name_single in genre_name.split():
+ genre_name_single = re.sub("'","&apos",genre_name_single)
+ sql_query = "INSERT INTO stations (server_name, listen_url, bitrate,
genre) VALUES ('%s','%s','%s','%s')" % (server_name, listen_url, bitrate,
genre_name_single)
+ sqlite_cur.execute(sql_query)
+
+ sqlite_con.commit()
+
+# Build the list of genres from DOM
+def buildGenreListDom(dom):
genre_hash = {}
genres = dom.getElementsByTagName("genre")
for genre in genres:
@@ -91,9 +165,17 @@ def buildGenreList(dom):
for key in sorted(genre_hash.keys()):
addDir(key, genre_hash[key])
+# Build the list of genres from SQLite
+def buildGenreListSQLite(sqlite_cur):
+ sqlite_cur.execute("SELECT genre, COUNT(*) AS cnt FROM stations GROUP BY
genre")
+ for genre, cnt in sqlite_cur:
+ addDir(genre, cnt)
+
# Add a genre to the list
def addDir(genre_name, count):
u = "%s?genre=%s" % (sys.argv[0], genre_name,)
+ # Try to unescape HTML-encoding; some strings need two passes - first to
convert "&" to "&" and second to unescape "&XYZ;"!
+ genre_name = unescapeString(genre_name)
genre_name_and_count = "%s (%u streams)" % (genre_name, count)
liz = xbmcgui.ListItem(genre_name_and_count, iconImage="DefaultFolder.png",
thumbnailImage="")
liz.setInfo( type="Music", infoLabels={ "Title":
genre_name_and_count,"Size": int(count)} )
@@ -101,8 +183,8 @@ def addDir(genre_name, count):
ok =
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=liz,isFolder=True)
return ok
-# Build list of links in a given genre
-def buildLinkList(dom, genre_name_orig):
+# Build list of links in a given genre from DOM
+def buildLinkListDom(dom, genre_name_orig):
entries = dom.getElementsByTagName("entry")
for entry in entries:
@@ -123,17 +205,32 @@ def buildLinkList(dom, genre_name_orig):
bitrate_objects = entry.getElementsByTagName("bitrate")
for bitrate_object in bitrate_objects:
- bitrate_string = getText(bitrate_object.childNodes)
- bitrate = re.sub('\D','',bitrate_string)
+ bitrate = getText(bitrate_object.childNodes)
addLink(server_name, listen_url, bitrate)
+# Build list of links in a given genre from SQLite
+def buildLinkListSQLite(sqlite_cur, genre_name_orig):
+ sql_query = "SELECT server_name, listen_url, bitrate FROM stations WHERE
genre='%s'" % (genre_name_orig)
+ sqlite_cur.execute(sql_query)
+ for server_name, listen_url, bitrate in sqlite_cur:
+ addLink(server_name, listen_url, bitrate)
+
# Add a link inside of a genre list
def addLink(server_name, listen_url, bitrate):
ok = True
+ # Try to unescape HTML-encoding; some strings need two passes - first to
convert "&" to "&" and second to unescape "&XYZ;"!
+ server_name = unescapeString(server_name)
+ listen_url = unescapeString(listen_url)
+ # Try to fix all incorrect values for bitrate (remove letters, reset to 0
etc.)
+ bitrate = re.sub('\D','',bitrate)
+ try:
+ bit = int(bitrate)
+ except:
+ bit = 0
u = "%s?play=%s" % (sys.argv[0], listen_url,)
liz = xbmcgui.ListItem(server_name, iconImage="DefaultVideo.png",
thumbnailImage="")
- liz.setInfo( type="Music", infoLabels={ "Title": server_name,"Size":
int(bitrate)} )
+ liz.setInfo( type="Music", infoLabels={ "Title": server_name,"Size": bit} )
liz.setProperty("IsPlayable","false");
ok =
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=liz,isFolder=False)
return ok
@@ -145,8 +242,8 @@ def readKbd():
if (kb.isConfirmed() and len(kb.getText()) > 2):
return kb.getText()
-# Do a search
-def doSearch(dom, query):
+# Do a search in DOM
+def doSearchDom(dom, query):
entries = dom.getElementsByTagName("entry")
for entry in entries:
@@ -167,11 +264,18 @@ def doSearch(dom, query):
bitrate_objects = entry.getElementsByTagName("bitrate")
for bitrate_object in bitrate_objects:
- bitrate_string = getText(bitrate_object.childNodes)
- bitrate = re.sub('\D','',bitrate_string)
+ bitrate = getText(bitrate_object.childNodes)
addLink(server_name, listen_url, bitrate)
+# Do a search in SQLite
+def doSearchSQLite(sqlite_cur, query):
+ sql_query = "SELECT server_name, listen_url, bitrate FROM stations WHERE
(genre LIKE '@@@%s@@@') OR (server_name LIKE '@@@%s@@@')" % (query, query)
+ sql_query = re.sub('@@@','%',sql_query)
+ sqlite_cur.execute(sql_query)
+ for server_name, listen_url, bitrate in sqlite_cur:
+ addLink(server_name, listen_url, bitrate)
+
# Play a link
def playLink(listen_url):
log("PLAY URL: %s" % listen_url )
@@ -198,6 +302,10 @@ def getParams():
# Logging
def log(msg):
xbmc.output("### [%s] - %s" % (__addonname__,msg,),level=xbmc.LOGDEBUG )
+
+# Log NOTICE
+def log_notice(msg):
+ xbmc.output("### [%s] - %s" % (__addonname__,msg,),level=xbmc.LOGNOTICE )
# Sorting
def sort(dir = False):
@@ -209,7 +317,105 @@ def sort(dir = False):
xbmcplugin.addSortMethod( handle=int( sys.argv[ 1 ] ),
sortMethod=xbmcplugin.SORT_METHOD_BITRATE, label2Mask="%X" )
xbmcplugin.endOfDirectory(int(sys.argv[1]))
+# Unescape escaped HTML characters
+def unescapeHTML(text):
+ def fixup(m):
+ text = m.group(0)
+ if text[:2] == "&#":
+ # character reference
+ try:
+ if text[:3] == "&#x":
+ return unichr(int(text[3:-1], 16))
+ else:
+ return unichr(int(text[2:-1]))
+ except ValueError:
+ pass
+ else:
+ # named entity
+ try:
+ text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
+ except KeyError:
+ pass
+ return text # leave as is
+ # Try to avoid broken UTF-8
+ try:
+ text = unicode(text, 'utf-8')
+ ret = re.sub("&#?\w+;", fixup, text)
+ except:
+ ret = text
+ return ret
+
+def unescapeXML(text):
+ try:
+ ret = unescape(text, {"'": "'", """: '"'})
+ except:
+ ret = text
+ return ret
+
+# Unesacpe wrapper
+def unescapeString(text):
+ pass1 = unescapeHTML(text)
+ pass2 = unescapeHTML(pass1)
+ pass3 = unescapeXML(pass2)
+ return pass3
+
+# Functions to read and write unix timestamp to database or file
+def putTimestampSQLite(sqlite_con, sqlite_cur):
+ unix_timestamp = int(time.time())
+ sql_line = "INSERT INTO updates (unix_timestamp) VALUES (%u)" %
(unix_timestamp)
+ sqlite_cur.execute(sql_line)
+ sqlite_con.commit()
+
+def putTimestampDom():
+ unix_timestamp = int(time.time())
+ timestamp_file_name = getTimestampFileName()
+ f = open(timestamp_file_name, 'w')
+ f.write(str(unix_timestamp))
+ f.close()
+
+def getTimestampSQLite(sqlite_cur):
+ sqlite_cur.execute("SELECT unix_timestamp FROM updates ORDER BY
unix_timestamp DESC LIMIT 1")
+ #unix_timestamp = sqlite_cur.fetchall()
+ for unix_timestamp in sqlite_cur:
+ return int(unix_timestamp[0])
+
+def getTimestampDom():
+ timestamp_file_name = getTimestampFileName()
+ try:
+ f = open(timestamp_file_name, 'r')
+ unix_timestamp = f.read()
+ f.close()
+ unix_timestamp = int(unix_timestamp)
+ except:
+ unix_timestamp = 0
+ return unix_timestamp
+
+# Timestamp wrappers
+def timestampExpiredSQLite(sqlite_cur):
+ current_unix_timestamp = int(time.time())
+ saved_unix_timestamp = getTimestampSQLite(sqlite_cur)
+ if (current_unix_timestamp - saved_unix_timestamp) > TIMESTAMP_THRESHOLD :
+ return 1
+ return 0
+
+def timestampExpiredDom():
+ current_unix_timestamp = int(time.time())
+ saved_unix_timestamp = getTimestampDom()
+ if (current_unix_timestamp - saved_unix_timestamp) > TIMESTAMP_THRESHOLD :
+ return 1
+ return 0
+
# MAIN
+
+# SQLite support - if available
+try:
+ from pysqlite2 import dbapi2 as sqlite
+ use_sqlite = 1
+ log_notice("Using SQLite!")
+except:
+ use_sqlite = 0
+ log_notice("SQLite not found -- reverting to older (and slower) text cache.")
+
params=getParams()
try:
@@ -230,24 +436,59 @@ iplay = len(play)
iinitial = len(initial)
if igenre > 1 :
- xml = readLocalXML()
- dom = parseXML(xml)
- buildLinkList(dom, genre)
+ if use_sqlite == 1:
+ sqlite_con, sqlite_cur, sqlite_is_emtpy = initSQLite()
+ timestamp_expired = timestampExpiredSQLite(sqlite_cur)
+ if timestamp_expired == 1:
+ xml = readRemoteXML()
+ dom = parseXML(xml)
+ DOMtoSQLite(dom, sqlite_con, sqlite_cur)
+ putTimestampSQLite(sqlite_con, sqlite_cur)
+ buildLinkListSQLite(sqlite_cur, genre)
+ else :
+ timestamp_expired = timestampExpiredDom()
+ if timestamp_expired == 1:
+ xml = readRemoteXML()
+ writeLocalXML(xml)
+ putTimestampDom()
+ else:
+ xml = readLocalXML()
+ dom = parseXML(xml)
+ buildLinkListDom(dom, genre)
sort()
elif iinitial > 1:
+ if use_sqlite == 1:
+ sqlite_con, sqlite_cur, sqlite_is_empty = initSQLite()
+ timestamp_expired = timestampExpiredSQLite(sqlite_cur)
+ if (sqlite_is_empty == 1) or (timestamp_expired == 1):
+ xml = readRemoteXML()
+ dom = parseXML(xml)
+ DOMtoSQLite(dom, sqlite_con, sqlite_cur)
+ putTimestampSQLite(sqlite_con, sqlite_cur)
+
+ elif use_sqlite == 0:
+ timestamp_expired = timestampExpiredDom()
+ if timestamp_expired == 1:
+ xml = readRemoteXML()
+ writeLocalXML(xml)
+ putTimestampDom()
+ elif timestamp_expired == 0:
+ xml = readLocalXML()
+ dom = parseXML(xml)
+
if initial == "search":
query = readKbd()
- xml = readRemoteXML()
- dom = parseXML(xml)
- writeLocalXML(xml)
- doSearch(dom, query)
+ if use_sqlite == 1:
+ doSearchSQLite(sqlite_cur, query)
+ else:
+ doSearchDom(dom, query)
sort()
elif initial == "list":
- xml = readRemoteXML()
- dom = parseXML(xml)
- writeLocalXML(xml)
- buildGenreList(dom)
+ if use_sqlite == 1:
+ buildGenreListSQLite(sqlite_cur)
+ else:
+ buildGenreListDom(dom)
sort(True)
elif iplay > 1:
-----------------------------------------------------------------------
Summary of changes:
plugin.audio.icecast/README.txt | 12 +-
plugin.audio.icecast/addon.xml | 3 +-
plugin.audio.icecast/default.py | 297 +++++++++++++++++++++++++++++++++++----
3 files changed, 278 insertions(+), 34 deletions(-)
mode change 100644 => 100755 plugin.audio.icecast/default.py
hooks/post-receive
--
Plugins
------------------------------------------------------------------------------
Learn how Oracle Real Application Clusters (RAC) One Node allows customers
to consolidate database storage, standardize their database environment, and,
should the need arise, upgrade to a full multi-node Oracle RAC database
without downtime or disruption
http://p.sf.net/sfu/oracle-sfdevnl
_______________________________________________
Xbmc-addons mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/xbmc-addons