Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

- Rewrote database layer so database is only access through a single …

…thread.

  This will hopefully solve some sporadic behaviour and 'Database is closed' errors.
- Removed JSON-URL and DR.dk sources as they weren't working anymore.
  • Loading branch information...
commit 57949c0cd6036bdeb05cd7c2ca4a37faa030ca69 1 parent f2cd786
@twinther authored
View
5 changelog.txt
@@ -1,3 +1,8 @@
+[B]Version 1.3.96 - 2013-01-xx[/B]
+- Rewrote database layer so database is only access through a single thread.
+ This will hopefully solve some sporadic behaviour and 'Database is closed' errors.
+- Removed JSON-URL and DR.dk sources as they weren't working anymore.
+
[B]Version 1.3.95 - 2013-01-06[/B]
- Updated Aeon Nox skin (thanks Lunatixz)
- Additional threading fixes for Frodo when invoking OSD/EPG while stream is playing
View
243 gui.py
@@ -19,6 +19,7 @@
#
import datetime
import threading
+import time
import xbmc
import xbmcgui
@@ -61,8 +62,6 @@
HALF_HOUR = datetime.timedelta(minutes = 30)
-ADDON = xbmcaddon.Addon(id = 'script.tvguide')
-
def debug(s):
if DEBUG: xbmc.log(str(s), xbmc.LOGDEBUG)
@@ -116,7 +115,6 @@ def __new__(cls):
def __init__(self):
super(TVGuide, self).__init__()
- self.source = None
self.notification = None
self.redrawingEPG = False
self.isClosing = False
@@ -126,6 +124,8 @@ def __init__(self):
self.focusPoint = Point()
self.epgView = EPGView()
self.streamingService = streaming.StreamsService()
+ self.player = xbmc.Player()
+ self.database = None
# add and removeControls were added post-eden
self.hasAddControls = hasattr(self, 'addControls')
@@ -158,11 +158,12 @@ def getControl(self, controlId):
def close(self):
if not self.isClosing:
self.isClosing = True
- if self.source:
- while self.source.updateInProgress:
- xbmc.sleep(500)
- self.source.close()
- super(TVGuide, self).close()
+ if self.player.isPlaying():
+ self.player.stop()
+ if self.database:
+ self.database.close(super(TVGuide, self).close)
+ else:
+ super(TVGuide, self).close()
@buggalo.buggalo_try_except({'method' : 'TVGuide.onInit'})
def onInit(self):
@@ -183,15 +184,21 @@ def onInit(self):
self.epgView.width = control.getWidth()
self.epgView.cellHeight = control.getHeight() / CHANNELS_PER_PAGE
- threading.Thread(target=self.initializeSourceInThread).start()
+ try:
+ self.database = src.Database()
+ except src.SourceNotConfiguredException:
+ self.onSourceNotConfigured()
+ self.close()
+ return
+ self.database.initialize(self.onSourceInitialized, self.isSourceInitializationCancelled)
self.updateTimebar()
@buggalo.buggalo_try_except({'method' : 'TVGuide.onAction'})
def onAction(self, action):
- if not self.source:
- if action.getId() in [ACTION_PARENT_DIR, KEY_NAV_BACK]:
- self.close()
- return
+# if not self.source:
+# if action.getId() in [ACTION_PARENT_DIR, KEY_NAV_BACK]:
+# self.close()
+# return
debug('Mode is: %s' % self.mode)
if self.mode == MODE_TV:
@@ -226,8 +233,7 @@ def onActionOSDMode(self, action):
self.onRedrawEPG(self.channelIdx, self.viewStartDate)
elif action.getId() == ACTION_SELECT_ITEM:
- if self.source.isPlayable(self.osdChannel):
- self._playChannel(self.osdChannel)
+ if self.playChannel(self.osdChannel):
self._hideOsd()
elif action.getId() == ACTION_PAGE_UP:
@@ -239,23 +245,23 @@ def onActionOSDMode(self, action):
self._showOsd()
elif action.getId() == ACTION_UP:
- self.osdChannel = self.source.getPreviousChannel(self.osdChannel)
- self.osdProgram = self.source.getCurrentProgram(self.osdChannel)
+ self.osdChannel = self.database.getPreviousChannel(self.osdChannel)
+ self.osdProgram = self.database.getCurrentProgram(self.osdChannel)
self._showOsd()
elif action.getId() == ACTION_DOWN:
- self.osdChannel = self.source.getNextChannel(self.osdChannel)
- self.osdProgram = self.source.getCurrentProgram(self.osdChannel)
+ self.osdChannel = self.database.getNextChannel(self.osdChannel)
+ self.osdProgram = self.database.getCurrentProgram(self.osdChannel)
self._showOsd()
elif action.getId() == ACTION_LEFT:
- previousProgram = self.source.getPreviousProgram(self.osdProgram)
+ previousProgram = self.database.getPreviousProgram(self.osdProgram)
if previousProgram:
self.osdProgram = previousProgram
self._showOsd()
elif action.getId() == ACTION_RIGHT:
- nextProgram = self.source.getNextProgram(self.osdProgram)
+ nextProgram = self.database.getNextProgram(self.osdProgram)
if nextProgram:
self.osdProgram = nextProgram
self._showOsd()
@@ -270,7 +276,7 @@ def onActionEPGMode(self, action):
return
elif action.getId() == KEY_CONTEXT_MENU:
- if self.source.isPlaying():
+ if self.player.isPlaying():
self._hideEpg()
controlInFocus = None
@@ -354,53 +360,53 @@ def onClick(self, controlId):
if program is None:
return
- if self.source.isPlayable(program.channel):
- self._playChannel(program.channel)
- else:
+ if not self.playChannel(program.channel):
result = self.streamingService.detectStream(program.channel)
if not result:
# could not detect stream, show context menu
self._showContextMenu(program)
elif type(result) == str:
# one single stream detected, save it and start streaming
- self.source.setCustomStreamUrl(program.channel, result)
- self._playChannel(program.channel)
+ def callback():
+ self.playChannel(program.channel)
+ self.database.setCustomStreamUrl(callback, program.channel, result)
+
else:
# multiple matches, let user decide
d = ChooseStreamAddonDialog(result)
d.doModal()
if d.stream is not None:
- self.source.setCustomStreamUrl(program.channel, d.stream)
- self._playChannel(program.channel)
+ def callback():
+ self.playChannel(program.channel)
+ self.database.setCustomStreamUrl(callback, program.channel, d.stream)
def _showContextMenu(self, program):
self._hideControl(self.C_MAIN_MOUSE_CONTROLS)
- d = PopupMenu(self.source, program, not program.notificationScheduled)
+ d = PopupMenu(self.database, program, not program.notificationScheduled)
d.doModal()
buttonClicked = d.buttonClicked
del d
if buttonClicked == PopupMenu.C_POPUP_REMIND:
if program.notificationScheduled:
- self.notification.delProgram(program)
+ self.notification.removeNotification(program)
else:
- self.notification.addProgram(program)
+ self.notification.addNotification(program)
self.onRedrawEPG(self.channelIdx, self.viewStartDate)
elif buttonClicked == PopupMenu.C_POPUP_CHOOSE_STREAM:
- d = StreamSetupDialog(self.source, program.channel)
+ d = StreamSetupDialog(self.database, program.channel)
d.doModal()
del d
elif buttonClicked == PopupMenu.C_POPUP_PLAY:
- if self.source.isPlayable(program.channel):
- self._playChannel(program.channel)
+ self.playChannel(program.channel)
elif buttonClicked == PopupMenu.C_POPUP_CHANNELS:
- d = ChannelsMenu(self.source)
+ d = ChannelsMenu(self.database)
d.doModal()
del d
self.onRedrawEPG(self.channelIdx, self.viewStartDate)
@@ -452,8 +458,8 @@ def onFocus(self, controlId):
if ADDON.getSetting('program.background.enabled') == 'true' and program.imageLarge is not None:
self.setControlImage(self.C_MAIN_BACKGROUND, program.imageLarge)
- if not self.osdEnabled and self.source.isPlaying():
- self.source.stop()
+ if not self.osdEnabled and self.player.isPlaying():
+ self.player.stop()
def _left(self, currentFocus):
control = self._findControlOnLeft(currentFocus)
@@ -501,41 +507,50 @@ def _previousDay(self):
def _moveUp(self, count = 1, scrollEvent = False):
if scrollEvent:
- self.onRedrawEPG(self.channelIdx - count, self.viewStartDate, scrollEvent = scrollEvent)
+ self.onRedrawEPG(self.channelIdx - count, self.viewStartDate)
else:
self.focusPoint.y = self.epgView.bottom
self.onRedrawEPG(self.channelIdx - count, self.viewStartDate, focusFunction = self._findControlAbove)
def _moveDown(self, count = 1, scrollEvent = False):
if scrollEvent:
- self.onRedrawEPG(self.channelIdx + count, self.viewStartDate, scrollEvent = scrollEvent)
+ self.onRedrawEPG(self.channelIdx + count, self.viewStartDate)
else:
self.focusPoint.y = self.epgView.top
self.onRedrawEPG(self.channelIdx + count, self.viewStartDate, focusFunction=self._findControlBelow)
def _channelUp(self):
- channel = self.source.getNextChannel(self.currentChannel)
- if self.source.isPlayable(channel):
- self._playChannel(channel)
+ channel = self.database.getNextChannel(self.currentChannel)
+ self.playChannel(channel)
def _channelDown(self):
- channel = self.source.getPreviousChannel(self.currentChannel)
- if self.source.isPlayable(channel):
- self._playChannel(channel)
+ channel = self.database.getPreviousChannel(self.currentChannel)
+ self.playChannel(channel)
- def _playChannel(self, channel):
+ def playChannel(self, channel):
self.currentChannel = channel
- wasPlaying = self.source.isPlaying()
- self.source.play(channel, self)
- for retry in range(0, 10):
- xbmc.sleep(100)
- if self.source.isPlaying():
- break
-
- if not wasPlaying and self.source.isPlaying():
+ wasPlaying = self.player.isPlaying()
+ url = self.database.getStreamUrl(channel)
+ if url:
+ self.player.play(item = url, windowed = self.osdEnabled)
+ for retry in range(0, 10):
+ xbmc.sleep(100)
+ if self.player.isPlaying():
+ break
+
+ threading.Timer(1, self.waitForPlayBackStopped).start()
+
+ if not wasPlaying and self.player.isPlaying():
self._hideEpg()
- self.osdProgram = self.source.getCurrentProgram(self.currentChannel)
+ self.osdProgram = self.database.getCurrentProgram(self.currentChannel)
+
+ return self.player.isPlaying()
+
+ def waitForPlayBackStopped(self):
+ while self.player.isPlaying():
+ time.sleep(0.5)
+ self.onPlayBackStopped()
def _showOsd(self):
if not self.osdEnabled:
@@ -566,8 +581,8 @@ def _hideEpg(self):
self.mode = MODE_TV
self._clearEpg()
- def onRedrawEPG(self, channelStart, startTime, scrollEvent = False, focusFunction = None):
- if self.redrawingEPG or self.source.updateInProgress or self.isClosing:
+ def onRedrawEPG(self, channelStart, startTime, focusFunction = None):
+ if self.redrawingEPG or self.database.updateInProgress or self.isClosing:
debug('onRedrawEPG - already redrawing')
return # ignore redraw request while redrawing
debug('onRedrawEPG')
@@ -584,10 +599,8 @@ def onRedrawEPG(self, channelStart, startTime, scrollEvent = False, focusFunctio
# remove existing controls
self._clearEpg()
- if self.source.isCacheExpired(startTime):
- self.redrawingEPG = False
- threading.Thread(target=self.updateSourceInThread, args=[channelStart, startTime, scrollEvent]).start()
- return
+
+ self.channelIdx, channels, programs = self.database.getEPGView(channelStart, startTime, self.onSourceProgressUpdate, clearExistingProgramList = False)
# date and time row
self.setControlLabel(self.C_MAIN_DATE, self.formatDate(self.viewStartDate))
@@ -595,30 +608,17 @@ def onRedrawEPG(self, channelStart, startTime, scrollEvent = False, focusFunctio
self.setControlLabel(4000 + col, self.formatTime(startTime))
startTime += HALF_HOUR
- # channels
- channels = self.source.getChannelList()
- if channelStart < 0:
- channelStart = len(channels) - 1
- elif channelStart > len(channels) - 1:
- channelStart = 0
-
- channelEnd = channelStart + CHANNELS_PER_PAGE
- self.channelIdx = channelStart
-
- channelsToShow = channels[channelStart : channelEnd]
- programs = self.source.getProgramList(channelsToShow, self.viewStartDate)
-
if programs is None:
self.onEPGLoadError()
return
# set channel logo or text
for idx in range(0, CHANNELS_PER_PAGE):
- if idx >= len(channelsToShow):
+ if idx >= len(channels):
self.setControlImage(4110 + idx, '')
self.setControlLabel(4010 + idx, '')
else:
- channel = channelsToShow[idx]
+ channel = channels[idx]
self.setControlLabel(4010 + idx, channel.title)
if channel.logo is not None:
self.setControlImage(4110 + idx, channel.logo)
@@ -626,7 +626,7 @@ def onRedrawEPG(self, channelStart, startTime, scrollEvent = False, focusFunctio
self.setControlImage(4110 + idx, '')
for program in programs:
- idx = channelsToShow.index(program.channel)
+ idx = channels.index(program.channel)
startDelta = program.startDate - self.viewStartDate
stopDelta = program.endDate - self.viewStartDate
@@ -719,9 +719,12 @@ def onSourceNotConfigured(self):
xbmcgui.Dialog().ok(strings(LOAD_ERROR_TITLE), strings(LOAD_ERROR_LINE1), strings(CONFIGURATION_ERROR_LINE2))
self.close()
- def onSourceInitialized(self, source):
- self.source = source
- self.notification = Notification(self.source, ADDON.getAddonInfo('path'))
+ def isSourceInitializationCancelled(self):
+ return xbmc.abortRequested or self.isClosing
+
+ def onSourceInitialized(self):
+ self.source = self.database.source # todo
+ self.notification = Notification(self.database, ADDON.getAddonInfo('path'))
self.setControlImage(self.C_MAIN_IMAGE, 'tvguide-logo-%s.png' % self.source.KEY)
self.onRedrawEPG(0, self.viewStartDate)
@@ -750,7 +753,7 @@ def onSourceProgressUpdate(self, percentageComplete):
return not xbmc.abortRequested and not self.isClosing
def onPlayBackStopped(self):
- if not self.source.isPlaying() and not self.isClosing:
+ if not self.player.isPlaying() and not self.isClosing:
self._hideControl(self.C_MAIN_OSD)
self.onRedrawEPG(self.channelIdx, self.viewStartDate)
@@ -886,32 +889,6 @@ def setControlText(self, controlId, text):
control.setText(text)
- def initializeSourceInThread(self):
- while not xbmc.abortRequested and not self.isClosing:
- try:
- source = src.instantiateSource(ADDON)
- xbmc.log("[script.tvguide] Using source: %s" % str(type(source)), xbmc.LOGDEBUG)
- self.onSourceInitialized(source)
- break
- except src.SourceNotConfiguredException:
- self.onSourceNotConfigured()
- break
- except src.SourceUpdateInProgressException, ex:
- xbmc.log('[script.tvguide] database update in progress...: %s' % str(ex), xbmc.LOGDEBUG)
- xbmc.sleep(1000)
- except Exception:
- buggalo.onExceptionRaised()
-
- def updateSourceInThread(self, channelStart, startTime, scrollEvent):
- try:
- self.source.updateChannelAndProgramListCaches(startTime, self.onSourceProgressUpdate, clearExistingProgramList = False)
- self.onRedrawEPG(channelStart, startTime, scrollEvent)
- except src.SourceException:
- self.onEPGLoadError()
- except Exception:
- buggalo.onExceptionRaised()
-
-
def updateTimebar(self, scheduleTimer = True):
try:
# move timebar to current time
@@ -938,19 +915,19 @@ class PopupMenu(xbmcgui.WindowXMLDialog):
C_POPUP_CHANNEL_TITLE = 4101
C_POPUP_PROGRAM_TITLE = 4102
- def __new__(cls, source, program, showRemind):
+ def __new__(cls, database, program, showRemind):
return super(PopupMenu, cls).__new__(cls, 'script-tvguide-menu.xml', ADDON.getAddonInfo('path'))
- def __init__(self, source, program, showRemind):
+ def __init__(self, database, program, showRemind):
"""
- @type source: source.Source
+ @type database: source.Database
@param program:
@type program: source.Program
@param showRemind:
"""
super(PopupMenu, self).__init__()
- self.source = source
+ self.database = database
self.program = program
self.showRemind = showRemind
self.buttonClicked = None
@@ -964,10 +941,10 @@ def onInit(self):
programTitleControl = self.getControl(self.C_POPUP_PROGRAM_TITLE)
playControl.setLabel(strings(WATCH_CHANNEL, self.program.channel.title))
- if not self.source.isPlayable(self.program.channel):
+ if not self.program.channel.isPlayable():
playControl.setEnabled(False)
self.setFocusId(self.C_POPUP_CHOOSE_STREAM)
- if self.source.getCustomStreamUrl(self.program.channel):
+ if self.database.getCustomStreamUrl(self.program.channel):
chooseStrmControl = self.getControl(self.C_POPUP_CHOOSE_STREAM)
chooseStrmControl.setLabel(strings(REMOVE_STRM_FILE))
@@ -993,12 +970,12 @@ def onAction(self, action):
@buggalo.buggalo_try_except({'method' : 'PopupMenu.onClick'})
def onClick(self, controlId):
- if controlId == self.C_POPUP_CHOOSE_STREAM and self.source.getCustomStreamUrl(self.program.channel):
- self.source.deleteCustomStreamUrl(self.program.channel)
+ if controlId == self.C_POPUP_CHOOSE_STREAM and self.database.getCustomStreamUrl(self.program.channel):
+ self.database.deleteCustomStreamUrl(self.program.channel)
chooseStrmControl = self.getControl(self.C_POPUP_CHOOSE_STREAM)
chooseStrmControl.setLabel(strings(CHOOSE_STRM_FILE))
- if not self.source.isPlayable(self.program.channel):
+ if not self.program.channel.isPlayable():
playControl = self.getControl(self.C_POPUP_PLAY)
playControl.setEnabled(False)
@@ -1016,17 +993,17 @@ class ChannelsMenu(xbmcgui.WindowXMLDialog):
C_CHANNELS_SAVE = 6003
C_CHANNELS_CANCEL = 6004
- def __new__(cls, source):
+ def __new__(cls, database):
return super(ChannelsMenu, cls).__new__(cls, 'script-tvguide-channels.xml', ADDON.getAddonInfo('path'))
- def __init__(self, source):
+ def __init__(self, database):
"""
- @type source: source.Source
+ @type database: source.Database
"""
super(ChannelsMenu, self).__init__()
- self.source = source
- self.channelList = source._retrieveChannelListFromDatabase(False)
+ self.database = database
+ self.channelList = database.getChannelList(onlyVisible = False)
self.swapInProgress = False
@buggalo.buggalo_try_except({'method' : 'ChannelsMenu.onInit'})
@@ -1081,8 +1058,7 @@ def onClick(self, controlId):
item.setIconImage(iconImage)
elif controlId == self.C_CHANNELS_SAVE:
- self.source._storeChannelListInDatabase(self.channelList)
- self.close()
+ self.database.saveChannelList(self.close, self.channelList)
elif controlId == self.C_CHANNELS_CANCEL:
self.close()
@@ -1165,16 +1141,16 @@ class StreamSetupDialog(xbmcgui.WindowXMLDialog):
VISIBLE_FAVOURITES = 'favourites'
VISIBLE_ADDONS = 'addons'
- def __new__(cls, source, channel):
+ def __new__(cls, database, channel):
return super(StreamSetupDialog, cls).__new__(cls, 'script-tvguide-streamsetup.xml', ADDON.getAddonInfo('path'))
- def __init__(self, source, channel):
+ def __init__(self, database, channel):
"""
- @type source: source.Source
+ @type database: source.Database
@type channel:source.Channel
"""
super(StreamSetupDialog, self).__init__()
- self.source = source
+ self.database = database
self.channel = channel
self.player = xbmc.Player()
@@ -1230,7 +1206,7 @@ def onClick(self, controlId):
if controlId == self.C_STREAM_STRM_BROWSE:
stream = xbmcgui.Dialog().browse(1, ADDON.getLocalizedString(30304), 'video', '.strm')
if stream:
- self.source.setCustomStreamUrl(self.channel, stream)
+ self.database.setCustomStreamUrl(None, self.channel, stream)
self.getControl(self.C_STREAM_STRM_FILE_LABEL).setText(stream)
self.strmFile = stream
@@ -1238,19 +1214,16 @@ def onClick(self, controlId):
listControl = self.getControl(self.C_STREAM_ADDONS_STREAMS)
item = listControl.getSelectedItem()
stream = item.getProperty('stream')
- self.source.setCustomStreamUrl(self.channel, stream)
- self.close()
+ self.database.setCustomStreamUrl(self.close, self.channel, stream)
elif controlId == self.C_STREAM_FAVOURITES_OK:
listControl = self.getControl(self.C_STREAM_FAVOURITES)
item = listControl.getSelectedItem()
stream = item.getProperty('stream')
- self.source.setCustomStreamUrl(self.channel, stream)
- self.close()
+ self.database.setCustomStreamUrl(self.close, self.channel, stream)
elif controlId == self.C_STREAM_STRM_OK:
- self.source.setCustomStreamUrl(self.channel, self.strmFile)
- self.close()
+ self.database.setCustomStreamUrl(self.close, self.channel, self.strmFile)
elif controlId in [self.C_STREAM_ADDONS_CANCEL, self.C_STREAM_FAVOURITES_CANCEL, self.C_STREAM_STRM_CANCEL]:
self.close()
View
74 notification.py
@@ -21,24 +21,25 @@
import os
import xbmc
import xbmcgui
+import source as src
from strings import *
class Notification(object):
- def __init__(self, source, addonPath):
- self.source = source
+ def __init__(self, database, addonPath):
+ """
+ @param database: source.Database
+ """
+ self.database = database
self.addonPath = addonPath
self.icon = os.path.join(self.addonPath, 'icon.png')
- # reuse conn for now todo less hacky
- self.conn = self.source.conn
-
def createAlarmClockName(self, programTitle, startTime):
return 'tvguide-%s-%s' % (programTitle, startTime)
def scheduleNotifications(self):
xbmc.log("[script.tvguide] Scheduling notifications")
- for channelTitle, programTitle, startTime in self.getAllNotifications():
+ for channelTitle, programTitle, startTime in self.database.getNotifications():
self._scheduleNotification(channelTitle, programTitle, startTime)
def _scheduleNotification(self, channelTitle, programTitle, startTime):
@@ -62,60 +63,23 @@ def _unscheduleNotification(self, programTitle, startTime):
xbmc.executebuiltin('CancelAlarm(%s-5mins,True)' % name.encode('utf-8', 'replace'))
xbmc.executebuiltin('CancelAlarm(%s-now,True)' % name.encode('utf-8', 'replace'))
- def addProgram(self, program):
- """
- @type program: source.program
- """
- c = self.conn.cursor()
- c.execute("INSERT INTO notifications(channel, program_title, source) VALUES(?, ?, ?)", [program.channel.id, program.title, self.source.KEY])
- self.conn.commit()
- c.close()
-
+ def addNotification(self, program):
+ self.database.addNotification(program)
self._scheduleNotification(program.channel.title, program.title, program.startDate)
- def delProgram(self, program):
- """
- @type program: source.program
- """
- c = self.conn.cursor()
- c.execute("DELETE FROM notifications WHERE channel=? AND program_title=? AND source=?", [program.channel.id, program.title, self.source.KEY])
- self.conn.commit()
- c.close()
-
+ def removeNotification(self, program):
+ self.database.removeNotification(program)
self._unscheduleNotification(program.title, program.startDate)
- def getAllNotifications(self, daysLimit = 2):
- start = datetime.datetime.now()
- end = start + datetime.timedelta(days = daysLimit)
- c = self.conn.cursor()
- c.execute("SELECT DISTINCT c.title, p.title, p.start_date FROM notifications n, channels c, programs p WHERE n.channel = c.id AND p.channel = c.id AND n.program_title = p.title AND n.source=? AND p.start_date >= ? AND p.end_date <= ?", [self.source.KEY, start, end])
- programs = c.fetchall()
- c.close()
-
- return programs
-
- def isNotificationRequiredForProgram(self, program):
- """
- @type program: source.program
- """
- c = self.conn.cursor()
- c.execute("SELECT 1 FROM notifications WHERE channel=? AND program_title=? AND source=?", [program.channel.id, program.title, self.source.KEY])
- result = c.fetchone()
- c.close()
-
- return result
-
- def clearAllNotifications(self):
- c = self.conn.cursor()
- c.execute('DELETE FROM notifications')
- self.conn.commit()
- c.close()
+if __name__ == '__main__':
+ database = src.Database()
+ def onNotificationsCleared():
+ xbmcgui.Dialog().ok(strings(CLEAR_NOTIFICATIONS), strings(DONE))
-if __name__ == '__main__':
- ADDON = xbmcaddon.Addon(id = 'script.tvguide')
- n = Notification(None, ADDON.getAddonInfo('path'))
- n.clearAllNotifications()
+ def onInitialized():
+ database.clearAllNotifications()
+ database.close(onNotificationsCleared)
- xbmcgui.Dialog().ok(strings(CLEAR_NOTIFICATIONS), strings(DONE))
+ database.initialize(onInitialized, None)
View
14 resources/settings.xml
@@ -2,22 +2,18 @@
<settings>
<category label="30101">
<setting id="source" label="30101" type="labelenum" default="YouSee.tv"
- values="YouSee.tv|DR.dk|XMLTV|ONTV.dk|JSON-URL" />
+ values="YouSee.tv|XMLTV|ONTV.dk" />
<setting id="youseetv.category" label="30102" type="labelenum" default="Danske"
values="Danske|Filmkanaler|Grundpakken|Mellempakken|Fuldpakken|Nordiske|Engelske|Tyske|Latinske|Slaviske|Tyrkiske|Kurdiske|Arabiske|Asiatiske"
visible="eq(-1,0)" />
<setting id="program.background.enabled" label="30107" type="bool" default="true" visible="eq(-2,0)"/>
- <setting id="xmltv.file" label="30103" type="file" visible="eq(-3,2)" />
- <setting id="xmltv.logo.folder" label="30116" type="folder" visible="eq(-4,2)"/>
+ <setting id="xmltv.file" label="30103" type="file" visible="eq(-3,1)" />
+ <setting id="xmltv.logo.folder" label="30116" type="folder" visible="eq(-4,1)"/>
- <setting id="ontv.url" label="30117" type="text" visible="eq(-5,3)" default="http://ontv.dk/xmltv/c81e728d9d4c2f636f067f89cc14862c"/>
-
- <setting id="json.url" label="30800" type="text" visible="eq(-6,4)" default="http://pvr.ne0.pl/xbmc/tvg-source.php"/>
-
- <setting id="weebtv.playback" label="30801" type="bool" default="true" visible="eq(-7,4)" enable="System.HasAddon(plugin.video.weeb.tv)" />
- </category>
+ <setting id="ontv.url" label="30117" type="text" visible="eq(-5,2)" default="http://ontv.dk/xmltv/c81e728d9d4c2f636f067f89cc14862c"/>
+ </category>
<category label="30114">
<setting id="cache.data.on.xbmc.startup" label="30110" type="bool" default="true" />
View
27 service.py
@@ -20,19 +20,28 @@
import xbmcaddon
import notification
import xbmc
-import source as src
+import source
+
+class Service(object):
+ def __init__(self):
+ self.database = source.Database()
+ self.database.initialize(self.onInit, None)
+
+ def onInit(self):
+ self.database.updateChannelAndProgramListCaches(self.onCachesUpdated)
+
+ def onCachesUpdated(self):
+
+ if ADDON.getSetting('notifications.enabled') == 'true':
+ n = notification.Notification(self.database, ADDON.getAddonInfo('path'))
+ n.scheduleNotifications()
+
+ self.database.close(None)
try:
ADDON = xbmcaddon.Addon(id = 'script.tvguide')
- source = src.instantiateSource(ADDON)
if ADDON.getSetting('cache.data.on.xbmc.startup') == 'true':
- channelList = None
- if source._isChannelListCacheExpired():
- channelList = source.updateChannelAndProgramListCaches()
-
- if ADDON.getSetting('notifications.enabled') == 'true':
- n = notification.Notification(source, ADDON.getAddonInfo('path'))
- n.scheduleNotifications()
+ Service()
except Exception, ex:
xbmc.log('[script.tvguide] Uncaugt exception in service.py: %s' % str(ex) , xbmc.LOGDEBUG)
View
644 source.py
@@ -20,20 +20,21 @@
import StringIO
import os
import threading
-import simplejson
import datetime
import time
import urllib2
from xml.etree import ElementTree
+import buggalo
+
from strings import *
+
import ysapi
-import buggalo
import xbmc
import xbmcgui
import xbmcvfs
from sqlite3 import dbapi2 as sqlite3
-SETTINGS_TO_CHECK = ['source', 'youseetv.category', 'xmltv.file', 'xmltv.logo.folder', 'ontv.url', 'json.url']
+SETTINGS_TO_CHECK = ['source', 'youseetv.category', 'xmltv.file', 'xmltv.logo.folder', 'ontv.url']
class Channel(object):
def __init__(self, id, title, logo = None, streamUrl = None, visible = True, weight = -1):
@@ -47,6 +48,9 @@ def __init__(self, id, title, logo = None, streamUrl = None, visible = True, wei
def isPlayable(self):
return hasattr(self, 'streamUrl') and self.streamUrl
+ def __eq__(self, other):
+ return self.id == other.id
+
def __repr__(self):
return 'Channel(id=%s, title=%s, logo=%s, streamUrl=%s)' \
% (self.id, self.title, self.logo, self.streamUrl)
@@ -80,9 +84,6 @@ def __repr__(self):
class SourceException(Exception):
pass
-class SourceUpdateInProgressException(SourceException):
- pass
-
class SourceUpdateCanceledException(SourceException):
pass
@@ -92,59 +93,127 @@ class SourceNotConfiguredException(SourceException):
class DatabaseSchemaException(sqlite3.DatabaseError):
pass
-class Source(object):
- KEY = "undefined"
+class Database(object):
SOURCE_DB = 'source.db'
- def __init__(self, addon, cachePath):
- self.cachePath = cachePath
+ def __init__(self):
+ self.conn = None
+ self.eventQueue = list()
+ self.event = threading.Event()
+ self.eventResults = dict()
+
+ self.source = instantiateSource()
+
self.updateInProgress = False
- buggalo.addExtraData('source', self.KEY)
- for key in SETTINGS_TO_CHECK:
- buggalo.addExtraData('setting: %s' % key, ADDON.getSetting(key))
+ self.updateFailed = False
+ self.sourceNotConfigured = False
+
+ #buggalo.addExtraData('source', self.source.KEY)
+ #for key in SETTINGS_TO_CHECK:
+ # buggalo.addExtraData('setting: %s' % key, ADDON.getSetting(key))
self.channelList = list()
- self.player = xbmc.Player()
- self.osdEnabled = addon.getSetting('enable.osd') == 'true'
- databasePath = os.path.join(self.cachePath, self.SOURCE_DB)
- for retries in range(0, 3):
+ profilePath = xbmc.translatePath(ADDON.getAddonInfo('profile'))
+ if not os.path.exists(profilePath):
+ os.makedirs(profilePath)
+ self.databasePath = os.path.join(profilePath, Database.SOURCE_DB)
+
+ threading.Thread(name = 'Database Event Loop', target = self.eventLoop).start()
+
+ def eventLoop(self):
+ print 'Database.eventLoop() >>>>>>>>>> starting...'
+ while True:
+ self.event.wait()
+ self.event.clear()
+
+ event = self.eventQueue.pop(0)
+
+ command = event[0]
+ callback = event[1]
+ resultReady = event[2]
+
+
+ print 'Database.eventLoop() >>>>>>>>>> processing command: ' + command.__name__
+
+ try:
+ result = command(*event[3:])
+ self.eventResults[command.__name__] = result
+
+ if callback:
+ threading.Thread(name = 'Database callback', target = callback).start()
+
+ if resultReady:
+ resultReady.set()
+
+ if self._close == command:
+ del self.eventQueue[:]
+ break
+
+
+ except Exception:
+ print 'Database.eventLoop() >>>>>>>>>> exception!'
+ buggalo.onExceptionRaised()
+
+ print 'Database.eventLoop() >>>>>>>>>> exiting...'
+
+ def initialize(self, callback, cancel_requested_callback):
+ self.eventQueue.append([self._initialize, callback, None, cancel_requested_callback])
+ self.event.set()
+
+ def _initialize(self, cancel_requested_callback):
+ self.sourceNotConfigured = False
+ while True:
+ if cancel_requested_callback is not None and cancel_requested_callback():
+ break
+
try:
- self.conn = sqlite3.connect(databasePath, detect_types=sqlite3.PARSE_DECLTYPES, check_same_thread = False)
+ self.conn = sqlite3.connect(self.databasePath, detect_types=sqlite3.PARSE_DECLTYPES)
self.conn.execute('PRAGMA foreign_keys = ON')
self.conn.row_factory = sqlite3.Row
# create and drop dummy table to check if database is locked
c = self.conn.cursor()
- c.execute('CREATE TABLE database_lock_check(id TEXT PRIMARY KEY)')
+ c.execute('CREATE TABLE IF NOT EXISTS database_lock_check(id TEXT PRIMARY KEY)')
c.execute('DROP TABLE database_lock_check')
c.close()
self._createTables()
- self.settingsChanged = self.wasSettingsChanged(addon)
+ self.settingsChanged = self._wasSettingsChanged(ADDON)
break
- except sqlite3.OperationalError, ex:
- raise SourceUpdateInProgressException(ex)
+ except sqlite3.OperationalError:
+ if cancel_requested_callback is None:
+ xbmc.log('[script.tvguide] Database is locked, bailing out...', xbmc.LOGDEBUG)
+ break
+ else: # ignore 'database is locked'
+ xbmc.log('[script.tvguide] Database is locked, retrying...', xbmc.LOGDEBUG)
+
except sqlite3.DatabaseError:
self.conn = None
try:
- os.unlink(databasePath)
+ os.unlink(self.databasePath)
except OSError:
pass
xbmcgui.Dialog().ok(ADDON.getAddonInfo('name'), strings(DATABASE_SCHEMA_ERROR_1),
strings(DATABASE_SCHEMA_ERROR_2), strings(DATABASE_SCHEMA_ERROR_3))
if self.conn is None:
- raise SourceNotConfiguredException()
+ self.sourceNotConfigured = True
- def close(self):
- #self.conn.rollback() # rollback any non-commit'ed changes to avoid database lock
- if self.player.isPlaying():
- self.player.stop()
+ def close(self, callback):
+ self.eventQueue.append([self._close, callback, None])
+ self.event.set()
+
+ def _close(self):
+ try:
+ # rollback any non-commit'ed changes to avoid database lock
+ self.conn.rollback()
+ except sqlite3.OperationalError:
+ pass # no transaction is active
self.conn.close()
- def wasSettingsChanged(self, addon):
+ def _wasSettingsChanged(self, addon):
settingsChanged = False
noRows = True
count = 0
@@ -172,25 +241,55 @@ def wasSettingsChanged(self, addon):
c.close()
print 'Settings changed: ' + str(settingsChanged)
- #return True # Uncomment to force cache regeneration every run, for debug prp only
return settingsChanged
- def getDataFromExternal(self, date, progress_callback = None):
- """
- Retrieve data from external as a list or iterable. Data may contain both Channel and Program objects.
- The source may choose to ignore the date parameter and return all data available.
- @param date: the date to retrieve the data for
- @param progress_callback:
- @return:
- """
- raise SourceException('getDataFromExternal not implemented!')
+ def _isCacheExpired(self, date):
+ if self.settingsChanged:
+ return True
- def isCacheExpired(self, date = datetime.datetime.now()):
- return self.settingsChanged or self._isChannelListCacheExpired() or self._isProgramListCacheExpired(date)
+ # check if channel data is up-to-date in database
+ try:
+ c = self.conn.cursor()
+ c.execute('SELECT channels_updated FROM sources WHERE id=?', [self.source.KEY]) # todo always fails with TypeError ??
+ row = c.fetchone()
+ if not row:
+ return True
+ lastUpdated = row['channels_updated']
+ c.close()
+
+ today = datetime.datetime.now()
+ if lastUpdated.day != today.day:
+ return True
+ except TypeError:
+ return True
+
+ # check if program data is up-to-date in database
+ dateStr = date.strftime('%Y-%m-%d')
+ c = self.conn.cursor()
+ c.execute('SELECT programs_updated FROM updates WHERE source=? AND date=?', [self.source.KEY, dateStr])
+ row = c.fetchone()
+ today = datetime.datetime.now()
+ expired = row is None or row['programs_updated'].day != today.day
+ c.close()
+
+ if not self.source.isUpdated(date):
+ # no reason to update if source has not been modified
+ return False
+
+ return expired
+
+
+ def updateChannelAndProgramListCaches(self, callback, date = datetime.datetime.now(), progress_callback = None, clearExistingProgramList = True):
+ self.eventQueue.append([self._updateChannelAndProgramListCaches, callback, None, date, progress_callback, clearExistingProgramList])
+ self.event.set()
+
+ def _updateChannelAndProgramListCaches(self, date, progress_callback, clearExistingProgramList):
+ if not self._isCacheExpired(date):
+ return
- def updateChannelAndProgramListCaches(self, date = datetime.datetime.now(), progress_callback = None, clearExistingProgramList = True):
self.updateInProgress = True
+ self.updateFailed = False
dateStr = date.strftime('%Y-%m-%d')
c = self.conn.cursor()
try:
@@ -199,22 +298,22 @@ def updateChannelAndProgramListCaches(self, date = datetime.datetime.now(), prog
progress_callback(0)
if self.settingsChanged:
- c.execute('DELETE FROM channels WHERE source=?', [self.KEY])
- c.execute('DELETE FROM programs WHERE source=?', [self.KEY])
- c.execute("DELETE FROM updates WHERE source=?", [self.KEY])
+ c.execute('DELETE FROM channels WHERE source=?', [self.source.KEY])
+ c.execute('DELETE FROM programs WHERE source=?', [self.source.KEY])
+ c.execute("DELETE FROM updates WHERE source=?", [self.source.KEY])
self.settingsChanged = False # only want to update once due to changed settings
if clearExistingProgramList:
- c.execute("DELETE FROM updates WHERE source=?", [self.KEY]) # cascades and deletes associated programs records
+ c.execute("DELETE FROM updates WHERE source=?", [self.source.KEY]) # cascades and deletes associated programs records
else:
- c.execute("DELETE FROM updates WHERE source=? AND date=?", [self.KEY, dateStr]) # cascades and deletes associated programs records
+ c.execute("DELETE FROM updates WHERE source=? AND date=?", [self.source.KEY, dateStr]) # cascades and deletes associated programs records
# programs updated
- c.execute("INSERT INTO updates(source, date, programs_updated) VALUES(?, ?, ?)", [self.KEY, dateStr, datetime.datetime.now()])
+ c.execute("INSERT INTO updates(source, date, programs_updated) VALUES(?, ?, ?)", [self.source.KEY, dateStr, datetime.datetime.now()])
updatesId = c.lastrowid
imported = imported_channels = imported_programs = 0
- for item in self.getDataFromExternal(date, progress_callback):
+ for item in self.source.getDataFromExternal(date, progress_callback):
imported += 1
if imported % 10000 == 0:
@@ -223,10 +322,10 @@ def updateChannelAndProgramListCaches(self, date = datetime.datetime.now(), prog
if isinstance(item, Channel):
imported_channels += 1
channel = item
- c.execute('INSERT OR IGNORE INTO channels(id, title, logo, stream_url, visible, weight, source) VALUES(?, ?, ?, ?, ?, (CASE ? WHEN -1 THEN (SELECT COALESCE(MAX(weight)+1, 0) FROM channels WHERE source=?) ELSE ? END), ?)', [channel.id, channel.title, channel.logo, channel.streamUrl, channel.visible, channel.weight, self.KEY, channel.weight, self.KEY])
+ c.execute('INSERT OR IGNORE INTO channels(id, title, logo, stream_url, visible, weight, source) VALUES(?, ?, ?, ?, ?, (CASE ? WHEN -1 THEN (SELECT COALESCE(MAX(weight)+1, 0) FROM channels WHERE source=?) ELSE ? END), ?)', [channel.id, channel.title, channel.logo, channel.streamUrl, channel.visible, channel.weight, self.source.KEY, channel.weight, self.source.KEY])
if not c.rowcount:
c.execute('UPDATE channels SET title=?, logo=?, stream_url=?, visible=(CASE ? WHEN -1 THEN visible ELSE ? END), weight=(CASE ? WHEN -1 THEN weight ELSE ? END) WHERE id=? AND source=?',
- [channel.title, channel.logo, channel.streamUrl, channel.weight, channel.visible, channel.weight, channel.weight, channel.id, self.KEY])
+ [channel.title, channel.logo, channel.streamUrl, channel.weight, channel.visible, channel.weight, channel.weight, channel.id, self.source.KEY])
elif isinstance(item, Program):
imported_programs += 1
@@ -237,22 +336,22 @@ def updateChannelAndProgramListCaches(self, date = datetime.datetime.now(), prog
channel = program.channel
c.execute('INSERT INTO programs(channel, title, start_date, end_date, description, image_large, image_small, source, updates_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)',
- [channel, program.title, program.startDate, program.endDate, program.description, program.imageLarge, program.imageSmall, self.KEY, updatesId])
+ [channel, program.title, program.startDate, program.endDate, program.description, program.imageLarge, program.imageSmall, self.source.KEY, updatesId])
# channels updated
- c.execute("UPDATE sources SET channels_updated=? WHERE id=?", [datetime.datetime.now(),self.KEY])
+ c.execute("UPDATE sources SET channels_updated=? WHERE id=?", [datetime.datetime.now(), self.source.KEY])
self.conn.commit()
if imported_channels == 0 or imported_programs == 0:
- raise SourceException('No channels or programs imported')
+ self.updateFailed = True
except SourceUpdateCanceledException:
# force source update on next load
- c.execute('UPDATE sources SET channels_updated=? WHERE id=?', [datetime.datetime.fromtimestamp(0), self.KEY])
- c.execute("DELETE FROM updates WHERE source=?", [self.KEY]) # cascades and deletes associated programs records
+ c.execute('UPDATE sources SET channels_updated=? WHERE id=?', [datetime.datetime.fromtimestamp(0), self.source.KEY])
+ c.execute("DELETE FROM updates WHERE source=?", [self.source.KEY]) # cascades and deletes associated programs records
self.conn.commit()
- except Exception, ex:
+ except Exception:
import traceback as tb
import sys
(type, value, traceback) = sys.exc_info()
@@ -265,19 +364,48 @@ def updateChannelAndProgramListCaches(self, date = datetime.datetime.now(), prog
try:
# invalidate cached data
- c.execute('UPDATE sources SET channels_updated=? WHERE id=?', [datetime.datetime.fromtimestamp(0), self.KEY])
+ c.execute('UPDATE sources SET channels_updated=? WHERE id=?', [datetime.datetime.fromtimestamp(0), self.source.KEY])
self.conn.commit()
except sqlite3.OperationalError:
pass # database is locked
- raise SourceException(ex)
+ self.updateFailed = True
finally:
self.updateInProgress = False
c.close()
+ def getEPGView(self, channelStart, date = datetime.datetime.now(), progress_callback = None, clearExistingProgramList = True):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._getEPGView, None, resultReady, channelStart, date, progress_callback, clearExistingProgramList])
+ self.event.set()
+
+ resultReady.wait()
+ if self.updateFailed:
+ raise SourceException('No channels or programs imported')
+
+ return self.eventResults.get(self._getEPGView.__name__)
+
+
+ def _getEPGView(self, channelStart, date, progress_callback, clearExistingProgramList):
+ self._updateChannelAndProgramListCaches(date, progress_callback, clearExistingProgramList)
+
+ channels = self._getChannelList(onlyVisible = True)
+
+ if channelStart < 0:
+ channelStart = len(channels) - 1
+ elif channelStart > len(channels) - 1:
+ channelStart = 0
+ channelEnd = channelStart + 9 # TODO constant
+ channelsOnPage = channels[channelStart : channelEnd]
+
+ programs = self._getProgramList(channelsOnPage, date)
+
+ return [channelStart, channelsOnPage, programs]
+
+
def getChannel(self, id):
c = self.conn.cursor()
- c.execute('SELECT * FROM channels WHERE source=? AND id=?', [self.KEY, id])
+ c.execute('SELECT * FROM channels WHERE source=? AND id=?', [self.source.KEY, id])
row = c.fetchone()
channel = Channel(row['id'], row['title'],row['logo'], row['stream_url'], row['visible'], row['weight'])
c.close()
@@ -300,31 +428,42 @@ def getPreviousChannel(self, currentChannel):
idx = len(channels) - 1
return channels[idx]
- def getChannelList(self):
- # cache channelList in memory
- if not self.channelList:
- self.channelList = self._retrieveChannelListFromDatabase()
+ def saveChannelList(self, callback, channelList):
+ self.eventQueue.append([self._saveChannelList, callback, None, channelList])
+ self.event.set()
- return self.channelList
-
- def _storeChannelListInDatabase(self, channelList):
+ def _saveChannelList(self, channelList):
c = self.conn.cursor()
for idx, channel in enumerate(channelList):
- c.execute('INSERT OR IGNORE INTO channels(id, title, logo, stream_url, visible, weight, source) VALUES(?, ?, ?, ?, ?, (CASE ? WHEN -1 THEN (SELECT COALESCE(MAX(weight)+1, 0) FROM channels WHERE source=?) ELSE ? END), ?)', [channel.id, channel.title, channel.logo, channel.streamUrl, channel.visible, channel.weight, self.KEY, channel.weight, self.KEY])
+ c.execute('INSERT OR IGNORE INTO channels(id, title, logo, stream_url, visible, weight, source) VALUES(?, ?, ?, ?, ?, (CASE ? WHEN -1 THEN (SELECT COALESCE(MAX(weight)+1, 0) FROM channels WHERE source=?) ELSE ? END), ?)', [channel.id, channel.title, channel.logo, channel.streamUrl, channel.visible, channel.weight, self.source.KEY, channel.weight, self.source.KEY])
if not c.rowcount:
- c.execute('UPDATE channels SET title=?, logo=?, stream_url=?, visible=?, weight=(CASE ? WHEN -1 THEN weight ELSE ? END) WHERE id=? AND source=?', [channel.title, channel.logo, channel.streamUrl, channel.visible, channel.weight, channel.weight, channel.id, self.KEY])
+ c.execute('UPDATE channels SET title=?, logo=?, stream_url=?, visible=?, weight=(CASE ? WHEN -1 THEN weight ELSE ? END) WHERE id=? AND source=?', [channel.title, channel.logo, channel.streamUrl, channel.visible, channel.weight, channel.weight, channel.id, self.source.KEY])
- c.execute("UPDATE sources SET channels_updated=? WHERE id=?", [datetime.datetime.now(), self.KEY])
+ c.execute("UPDATE sources SET channels_updated=? WHERE id=?", [datetime.datetime.now(), self.source.KEY])
self.channelList = None
self.conn.commit()
- def _retrieveChannelListFromDatabase(self, onlyVisible = True):
+ def getChannelList(self, onlyVisible = True):
+ if not self.channelList or not onlyVisible:
+ resultReady = threading.Event()
+ self.eventQueue.append([self._getChannelList, None, resultReady, onlyVisible])
+ self.event.set()
+
+ resultReady.wait()
+ if not onlyVisible:
+ return self.eventResults.get(self._getChannelList.__name__)
+
+ self.channelList = self.eventResults.get(self._getChannelList.__name__)
+ return self.channelList
+
+
+ def _getChannelList(self, onlyVisible):
c = self.conn.cursor()
channelList = list()
if onlyVisible:
- c.execute('SELECT * FROM channels WHERE source=? AND visible=? ORDER BY weight', [self.KEY, True])
+ c.execute('SELECT * FROM channels WHERE source=? AND visible=? ORDER BY weight', [self.source.KEY, True])
else:
- c.execute('SELECT * FROM channels WHERE source=? ORDER BY weight', [self.KEY])
+ c.execute('SELECT * FROM channels WHERE source=? ORDER BY weight', [self.source.KEY])
for row in c:
channel = Channel(row['id'], row['title'],row['logo'], row['stream_url'], row['visible'], row['weight'])
channelList.append(channel)
@@ -334,7 +473,7 @@ def _retrieveChannelListFromDatabase(self, onlyVisible = True):
def _isChannelListCacheExpired(self):
try:
c = self.conn.cursor()
- c.execute('SELECT channels_updated FROM sources WHERE id=?', [self.KEY])
+ c.execute('SELECT channels_updated FROM sources WHERE id=?', [self.source.KEY])
row = c.fetchone()
if not row:
return True
@@ -347,6 +486,14 @@ def _isChannelListCacheExpired(self):
return True
def getCurrentProgram(self, channel):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._getCurrentProgram, None, resultReady, channel])
+ self.event.set()
+
+ resultReady.wait()
+ return self.eventResults.get(self._getCurrentProgram.__name__)
+
+ def _getCurrentProgram(self, channel):
"""
@param channel:
@@ -356,7 +503,7 @@ def getCurrentProgram(self, channel):
program = None
now = datetime.datetime.now()
c = self.conn.cursor()
- c.execute('SELECT * FROM programs WHERE channel=? AND source=? AND start_date <= ? AND end_date >= ?', [channel.id, self.KEY, now, now])
+ c.execute('SELECT * FROM programs WHERE channel=? AND source=? AND start_date <= ? AND end_date >= ?', [channel.id, self.source.KEY, now, now])
row = c.fetchone()
if row:
program = Program(channel, row['title'], row['start_date'], row['end_date'], row['description'], row['image_large'], row['image_small'])
@@ -364,10 +511,18 @@ def getCurrentProgram(self, channel):
return program
- def getNextProgram(self, program):
+ def getNextProgram(self, channel):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._getNextProgram, None, resultReady, channel])
+ self.event.set()
+
+ resultReady.wait()
+ return self.eventResults.get(self._getNextProgram.__name__)
+
+ def _getNextProgram(self, program):
nextProgram = None
c = self.conn.cursor()
- c.execute('SELECT * FROM programs WHERE channel=? AND source=? AND start_date >= ? ORDER BY start_date ASC LIMIT 1', [program.channel.id, self.KEY, program.endDate])
+ c.execute('SELECT * FROM programs WHERE channel=? AND source=? AND start_date >= ? ORDER BY start_date ASC LIMIT 1', [program.channel.id, self.source.KEY, program.endDate])
row = c.fetchone()
if row:
nextProgram = Program(program.channel, row['title'], row['start_date'], row['end_date'], row['description'], row['image_large'], row['image_small'])
@@ -375,10 +530,18 @@ def getNextProgram(self, program):
return nextProgram
- def getPreviousProgram(self, program):
+ def getPreviousProgram(self, channel):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._getPreviousProgram, None, resultReady, channel])
+ self.event.set()
+
+ resultReady.wait()
+ return self.eventResults.get(self._getPreviousProgram.__name__)
+
+ def _getPreviousProgram(self, program):
previousProgram = None
c = self.conn.cursor()
- c.execute('SELECT * FROM programs WHERE channel=? AND source=? AND end_date <= ? ORDER BY start_date DESC LIMIT 1', [program.channel.id, self.KEY, program.startDate])
+ c.execute('SELECT * FROM programs WHERE channel=? AND source=? AND end_date <= ? ORDER BY start_date DESC LIMIT 1', [program.channel.id, self.source.KEY, program.startDate])
row = c.fetchone()
if row:
previousProgram = Program(program.channel, row['title'], row['start_date'], row['end_date'], row['description'], row['image_large'], row['image_small'])
@@ -387,6 +550,14 @@ def getPreviousProgram(self, program):
return previousProgram
def getProgramList(self, channels, startTime):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._getProgramList, None, resultReady, channels, startTime])
+ self.event.set()
+
+ resultReady.wait()
+ return self.eventResults.get(self._getProgramList.__name__)
+
+ def _getProgramList(self, channels, startTime):
"""
@param channels:
@@ -403,18 +574,19 @@ def getProgramList(self, channels, startTime):
channelMap[c.id] = c
c = self.conn.cursor()
- c.execute('SELECT p.*, (SELECT 1 FROM notifications n WHERE n.channel=p.channel AND n.program_title=p.title AND n.source=p.source) AS notification_scheduled FROM programs p WHERE p.channel IN (\'' + ('\',\''.join(channelMap.keys())) + '\') AND p.source=? AND p.end_date >= ? AND p.start_date <= ?', [self.KEY, startTime, endTime])
+ c.execute('SELECT p.*, (SELECT 1 FROM notifications n WHERE n.channel=p.channel AND n.program_title=p.title AND n.source=p.source) AS notification_scheduled FROM programs p WHERE p.channel IN (\'' + ('\',\''.join(channelMap.keys())) + '\') AND p.source=? AND p.end_date >= ? AND p.start_date <= ?', [self.source.KEY, startTime, endTime])
for row in c:
program = Program(channelMap[row['channel']], row['title'], row['start_date'], row['end_date'], row['description'], row['image_large'], row['image_small'], row['notification_scheduled'])
programList.append(program)
+
return programList
def _isProgramListCacheExpired(self, date = datetime.datetime.now()):
# check if data is up-to-date in database
dateStr = date.strftime('%Y-%m-%d')
c = self.conn.cursor()
- c.execute('SELECT programs_updated FROM updates WHERE source=? AND date=?', [self.KEY, dateStr])
+ c.execute('SELECT programs_updated FROM updates WHERE source=? AND date=?', [self.source.KEY, dateStr])
row = c.fetchone()
today = datetime.datetime.now()
expired = row is None or row['programs_updated'].day != today.day
@@ -422,14 +594,11 @@ def _isProgramListCacheExpired(self, date = datetime.datetime.now()):
return expired
- def _downloadUrl(self, url):
- u = urllib2.urlopen(url, timeout=30)
- content = u.read()
- u.close()
-
- return content
+ def setCustomStreamUrl(self, callback, channel, stream_url):
+ self.eventQueue.append([self._setCustomStreamUrl, callback, None, channel, stream_url])
+ self.event.set()
- def setCustomStreamUrl(self, channel, stream_url):
+ def _setCustomStreamUrl(self, channel, stream_url):
c = self.conn.cursor()
c.execute("DELETE FROM custom_stream_url WHERE channel=?", [channel.id])
c.execute("INSERT INTO custom_stream_url(channel, stream_url) VALUES(?, ?)", [channel.id, stream_url.decode('utf-8', 'ignore')])
@@ -437,6 +606,14 @@ def setCustomStreamUrl(self, channel, stream_url):
c.close()
def getCustomStreamUrl(self, channel):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._getCustomStreamUrl, None, resultReady, channel])
+ self.event.set()
+
+ resultReady.wait()
+ return self.eventResults.get(self._getCustomStreamUrl.__name__)
+
+ def _getCustomStreamUrl(self, channel):
c = self.conn.cursor()
c.execute("SELECT stream_url FROM custom_stream_url WHERE channel=?", [channel.id])
stream_url = c.fetchone()
@@ -453,35 +630,17 @@ def deleteCustomStreamUrl(self, channel):
self.conn.commit()
c.close()
- def isPlayable(self, channel):
- customStreamUrl = self.getCustomStreamUrl(channel)
- return customStreamUrl is not None or channel.isPlayable()
-
- def isPlaying(self):
- return self.player.isPlaying()
-
- def stop(self):
- self.player.stop()
-
- def play(self, channel, playBackStoppedHandler):
+ def getStreamUrl(self, channel):
customStreamUrl = self.getCustomStreamUrl(channel)
if customStreamUrl:
customStreamUrl = customStreamUrl.encode('utf-8', 'ignore')
- xbmc.log("Playing custom stream url: %s" % customStreamUrl)
- self.player.play(item = customStreamUrl, windowed = self.osdEnabled)
+ return customStreamUrl
elif channel.isPlayable():
streamUrl = channel.streamUrl.encode('utf-8', 'ignore')
- xbmc.log("Playing : %s" % streamUrl)
- self.player.play(item = streamUrl, windowed = self.osdEnabled)
-
- threading.Timer(1, self.waitForPlayBackStopped, [playBackStoppedHandler]).start()
+ return streamUrl
- @buggalo.buggalo_try_except
- def waitForPlayBackStopped(self, playBackStoppedHandler):
- while self.player.isPlaying():
- time.sleep(0.5)
- playBackStoppedHandler.onPlayBackStopped()
+ return None
def _createTables(self):
c = self.conn.cursor()
@@ -526,7 +685,7 @@ def _createTables(self):
c.execute('CREATE INDEX end_date_idx ON programs(end_date)')
# make sure we have a record in sources for this Source
- c.execute("INSERT OR IGNORE INTO sources(id, channels_updated) VALUES(?, ?)", [self.KEY, datetime.datetime.fromtimestamp(0)])
+ c.execute("INSERT OR IGNORE INTO sources(id, channels_updated) VALUES(?, ?)", [self.source.KEY, datetime.datetime.fromtimestamp(0)])
self.conn.commit()
c.close()
@@ -534,47 +693,122 @@ def _createTables(self):
except sqlite3.OperationalError, ex:
raise DatabaseSchemaException(ex)
-class DrDkSource(Source):
- KEY = 'drdk'
- CHANNELS_URL = 'http://www.dr.dk/tjenester/programoversigt/dbservice.ashx/getChannels?type=tv'
- PROGRAMS_URL = 'http://www.dr.dk/tjenester/programoversigt/dbservice.ashx/getSchedule?channel_source_url=%s&broadcastDate=%s'
- def __init__(self, addon, cachePath):
- super(DrDkSource, self).__init__(addon, cachePath)
+ def addNotification(self, program):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._addNotification, None, resultReady, program])
+ self.event.set()
- def getDataFromExternal(self, date, progress_callback = None):
- jsonChannels = simplejson.loads(self._downloadUrl(self.CHANNELS_URL))
+ resultReady.wait()
+ # no result, but block until operation is done
- channels = jsonChannels['result']
- for idx, channel in enumerate(channels):
- c = Channel(id = channel['source_url'], title = channel['name'])
- yield c
+ def _addNotification(self, program):
+ """
+ @type program: source.program
+ """
+ c = self.conn.cursor()
+ c.execute("INSERT INTO notifications(channel, program_title, source) VALUES(?, ?, ?)", [program.channel.id, program.title, self.source.KEY])
+ self.conn.commit()
+ c.close()
- url = self.PROGRAMS_URL % (channel['source_url'].replace('+', '%2b'), date.strftime('%Y-%m-%dT00:00:00'))
- jsonPrograms = simplejson.loads(self._downloadUrl(url))
- for program in jsonPrograms['result']:
- if program.has_key('ppu_description'):
- description = program['ppu_description']
- else:
- description = strings(NO_DESCRIPTION)
+ def removeNotification(self, program):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._removeNotification, None, resultReady, program])
+ self.event.set()
- p = Program(c, program['pro_title'], self._parseDate(program['pg_start']), self._parseDate(program['pg_stop']), description)
- yield p
+ resultReady.wait()
+ # no result, but block until operation is done
- if progress_callback:
- if not progress_callback(100.0 / len(channels) * idx):
- raise SourceUpdateCanceledException()
+ def _removeNotification(self, program):
+ """
+ @type program: source.program
+ """
+ c = self.conn.cursor()
+ c.execute("DELETE FROM notifications WHERE channel=? AND program_title=? AND source=?", [program.channel.id, program.title, self.source.KEY])
+ self.conn.commit()
+ c.close()
- def _parseDate(self, dateString):
- t = time.strptime(dateString[:19], '%Y-%m-%dT%H:%M:%S')
- return datetime.datetime(t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)
+
+ def getNotifications(self, daysLimit = 2):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._getNotifications, None, resultReady, daysLimit])
+ self.event.set()
+
+ resultReady.wait()
+ return self.eventResults.get(self._getNotifications.__name__)
+
+ def _getNotifications(self, daysLimit):
+ start = datetime.datetime.now()
+ end = start + datetime.timedelta(days = daysLimit)
+ c = self.conn.cursor()
+ c.execute("SELECT DISTINCT c.title, p.title, p.start_date FROM notifications n, channels c, programs p WHERE n.channel = c.id AND p.channel = c.id AND n.program_title = p.title AND n.source=? AND p.start_date >= ? AND p.end_date <= ?", [self.source.KEY, start, end])
+ programs = c.fetchall()
+ c.close()
+
+ return programs
+
+ def isNotificationRequiredForProgram(self, program):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._isNotificationRequiredForProgram, None, resultReady, program])
+ self.event.set()
+
+ resultReady.wait()
+ return self.eventResults.get(self._isNotificationRequiredForProgram.__name__)
+
+ def _isNotificationRequiredForProgram(self, program):
+ """
+ @type program: source.program
+ """
+ c = self.conn.cursor()
+ c.execute("SELECT 1 FROM notifications WHERE channel=? AND program_title=? AND source=?", [program.channel.id, program.title, self.source.KEY])
+ result = c.fetchone()
+ c.close()
+
+ return result
+
+ def clearAllNotifications(self):
+ resultReady = threading.Event()
+ self.eventQueue.append([self._clearAllNotifications, None, resultReady])
+ self.event.set()
+
+ resultReady.wait()
+ # no result, but block until operation is done
+
+ def _clearAllNotifications(self):
+ c = self.conn.cursor()
+ c.execute('DELETE FROM notifications')
+ self.conn.commit()
+ c.close()
+
+
+
+class Source(object):
+ def getDataFromExternal(self, date, progress_callback = None):
+ """
+ Retrieve data from external as a list or iterable. Data may contain both Channel and Program objects.
+ The source may choose to ignore the date parameter and return all data available.
+
+ @param date: the date to retrieve the data for
+ @param progress_callback:
+ @return:
+ """
+ return None
+
+ def isUpdated(self, lastUpdated):
+ return True
+
+ def _downloadUrl(self, url):
+ u = urllib2.urlopen(url, timeout=30)
+ content = u.read()
+ u.close()
+
+ return content
class YouSeeTvSource(Source):
KEY = 'youseetv'
- def __init__(self, addon, cachePath):
- super(YouSeeTvSource, self).__init__(addon, cachePath)
+ def __init__(self, addon):
self.date = datetime.datetime.today()
self.channelCategory = addon.getSetting('youseetv.category')
self.ysApi = ysapi.YouSeeTVGuideApi()
@@ -615,8 +849,7 @@ def _parseDate(self, dateString):
class XMLTVSource(Source):
KEY = 'xmltv'
- def __init__(self, addon, cachePath):
- super(XMLTVSource, self).__init__(addon, cachePath)
+ def __init__(self, addon):
self.logoFolder = addon.getSetting('xmltv.logo.folder')
self.xmltvFile = addon.getSetting('xmltv.file')
@@ -628,7 +861,7 @@ def getDataFromExternal(self, date, progress_callback = None):
context = ElementTree.iterparse(f, events=("start", "end"))
return parseXMLTV(context, f, f.size, self.logoFolder, progress_callback)
- def _isChannelListCacheExpired(self):
+ def isUpdated(self, lastUpdated):
if hasattr(xbmcvfs, 'stat'):
# we cannot determine this without the stat function
# https://github.com/xbmc/xbmc/pull/1062
@@ -636,28 +869,14 @@ def _isChannelListCacheExpired(self):
else:
mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime = os.stat(self.xmltvFile)
- try:
- c = self.conn.cursor()
- c.execute('SELECT channels_updated FROM sources WHERE id=?', [self.KEY])
- row = c.fetchone()
- if not row:
- return True
- lastUpdated = row['channels_updated']
- c.close()
- except TypeError:
- return True
-
fileUpdated = datetime.datetime.fromtimestamp(mtime)
return fileUpdated > lastUpdated
- def _isProgramListCacheExpired(self, date = datetime.datetime.now()):
- return self._isChannelListCacheExpired()
class ONTVSource(Source):
KEY = 'ontv'
- def __init__(self, addon, cachePath):
- super(ONTVSource, self).__init__(addon, cachePath)
+ def __init__(self, addon):
self.ontvUrl = addon.getSetting('ontv.url')
def getDataFromExternal(self, date, progress_callback = None):
@@ -666,98 +885,6 @@ def getDataFromExternal(self, date, progress_callback = None):
context = ElementTree.iterparse(io)
return parseXMLTV(context, io, len(xml), None, progress_callback)
- def _isProgramListCacheExpired(self, date = datetime.datetime.now()):
- return self._isChannelListCacheExpired()
-
-
-class JSONSource(Source):
- KEY = 'json-url'
-
- def __init__(self, addon, cachePath):
- super(JSONSource, self).__init__(addon, cachePath)
- self.playbackUsingWeebTv = False
- self.JSONURL = addon.getSetting('json.url')
-
- if not addon.getSetting('json.url'):
- raise SourceNotConfiguredException()
-
- try:
- if addon.getSetting('weebtv.playback') == 'true':
- xbmcaddon.Addon(id = 'plugin.video.weeb.tv') # raises Exception if addon is not installed
- self.playbackUsingWeebTv = True
- except Exception:
- ADDON.setSetting('weebtv.playback', 'false')
- xbmcgui.Dialog().ok(ADDON.getAddonInfo('name'), strings(WEEBTV_WEBTV_MISSING_1),
- strings(WEEBTV_WEBTV_MISSING_2), strings(WEEBTV_WEBTV_MISSING_3))
-
- def getDataFromExternal(self, date, progress_callback = None):
- url = self.JSONURL + '?d=' + date.strftime('%Y-%m-%d')
- print 'Load JSON URL: ' + url
- try:
- r = urllib2.Request(url)
- u = urllib2.urlopen(r)
- json = u.read()
- u.close()
-
- channels = simplejson.loads(json)
- except urllib2.URLError, e:
- raise SourceException('Failed to fetch JSON: ' + str(e))
- except Exception:
- raise SourceException('Invalid JSON source (failed to parse output)')
-
-
- for idx, ch in enumerate(channels):
- try:
- print 'Parsing channel: ' + ch['n'].encode("utf-8","ignore")
- except KeyError:
- print ch
- #try:
- if ch.has_key('l') and ch['l'] is not None: # Channel logo
- ch['l'] = str(ch['l'])
- else:
- ch['l'] = None
-
- c = Channel(id = ch['i'], title = ch['n'], logo = ch['l'])
-
- if self.playbackUsingWeebTv and ch.has_key('c') and ch['c'] is not None: # channel numeric id
- c.streamUrl = 'plugin://plugin.video.weeb.tv/?mode=2&action=1&cid=' + str(ch['c']) + '&title=' + str(ch['n'])
- yield c
-
- print 'Found ' + str(len(ch['p'])) + ' programs'
- if ch.has_key('p') and len(ch['p']) > 0:
- for pr in ch['p']:
- #print 'Parsing program: ' + pr['t'].encode("utf-8","ignore")
- if pr.has_key('d') and pr['d'] is not None: # program description
- description = pr['d']
- else:
- description = strings(NO_DESCRIPTION)
-
- if not pr.has_key('l'): # large program image
- pr['l'] = None
-
- if not pr.has_key('i'): # small program image aka icon
- pr['i'] = None
-
- p = Program(
- c,
- pr['t'],
- self._parseDate(pr['s']),
- self._parseDate(pr['e']),
- description,
- pr['l'],
- pr['i']
- )
- yield p
-
-
- if progress_callback:
- if not progress_callback(100.0 / len(channels) * idx):
- raise SourceUpdateCanceledException()
- #except Exception:
- # raise SourceException('External JSON looks invalid, error detected in element ' + str(idx))
-
- def _parseDate(self, dateString):
- return datetime.datetime.fromtimestamp(dateString)
def parseXMLTVDate(dateString):
if dateString is not None:
@@ -833,27 +960,18 @@ def read(self, bytes):
def tell(self):
return self.bytesRead
-
-
-def instantiateSource(addon):
+def instantiateSource():
SOURCES = {
'YouSee.tv' : YouSeeTvSource,
- 'DR.dk' : DrDkSource,
'XMLTV' : XMLTVSource,
- 'ONTV.dk' : ONTVSource,
- 'JSON-URL' : JSONSource
+ 'ONTV.dk' : ONTVSource
}
- cachePath = xbmc.translatePath(ADDON.getAddonInfo('profile'))
-
- if not os.path.exists(cachePath):
- os.makedirs(cachePath)
-
try:
- activeSource = SOURCES[addon.getSetting('source')]
+ activeSource = SOURCES[ADDON.getSetting('source')]
except KeyError:
activeSource = SOURCES['YouSee.tv']
- return activeSource(addon, cachePath)
+ return activeSource(ADDON)
Please sign in to comment.
Something went wrong with that request. Please try again.