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 "&amp;" 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 "&amp;" 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, {"&apos;": "'", "&quot;": '"'})
+  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

Reply via email to