summaryrefslogtreecommitdiff
path: root/plugin.video.mediathekview/classes
diff options
context:
space:
mode:
Diffstat (limited to 'plugin.video.mediathekview/classes')
-rw-r--r--plugin.video.mediathekview/classes/__init__.py0
-rw-r--r--plugin.video.mediathekview/classes/channel.py11
-rw-r--r--plugin.video.mediathekview/classes/channelui.py42
-rw-r--r--plugin.video.mediathekview/classes/exceptions.py9
-rw-r--r--plugin.video.mediathekview/classes/film.py21
-rw-r--r--plugin.video.mediathekview/classes/filmui.py105
-rw-r--r--plugin.video.mediathekview/classes/initialui.py47
-rw-r--r--plugin.video.mediathekview/classes/mvupdate.py200
-rw-r--r--plugin.video.mediathekview/classes/notifier.py42
-rw-r--r--plugin.video.mediathekview/classes/settings.py37
-rw-r--r--plugin.video.mediathekview/classes/show.py13
-rw-r--r--plugin.video.mediathekview/classes/showui.py47
-rw-r--r--plugin.video.mediathekview/classes/store.py127
-rw-r--r--plugin.video.mediathekview/classes/storemysql.py534
-rw-r--r--plugin.video.mediathekview/classes/storesqlite.py766
-rw-r--r--plugin.video.mediathekview/classes/ttml2srt.py221
-rw-r--r--plugin.video.mediathekview/classes/updater.py431
17 files changed, 2653 insertions, 0 deletions
diff --git a/plugin.video.mediathekview/classes/__init__.py b/plugin.video.mediathekview/classes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.mediathekview/classes/__init__.py
diff --git a/plugin.video.mediathekview/classes/channel.py b/plugin.video.mediathekview/classes/channel.py
new file mode 100644
index 0000000..d142383
--- /dev/null
+++ b/plugin.video.mediathekview/classes/channel.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+
+# -- Classes ------------------------------------------------
+class Channel( object ):
+ def __init__( self ):
+ self.id = 0
+ self.channel = '' \ No newline at end of file
diff --git a/plugin.video.mediathekview/classes/channelui.py b/plugin.video.mediathekview/classes/channelui.py
new file mode 100644
index 0000000..7061013
--- /dev/null
+++ b/plugin.video.mediathekview/classes/channelui.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import sys, urllib
+import xbmcplugin, xbmcgui
+
+from classes.channel import Channel
+from classes.settings import Settings
+
+# -- Classes ------------------------------------------------
+class ChannelUI( Channel ):
+ def __init__( self, handle, sortmethods = [ xbmcplugin.SORT_METHOD_TITLE ], next = 'initial' ):
+ self.base_url = sys.argv[0]
+ self.next = next
+ self.handle = handle
+ self.sortmethods = sortmethods
+ self.count = 0
+
+ def Begin( self ):
+ for method in self.sortmethods:
+ xbmcplugin.addSortMethod( self.handle, method )
+
+ def Add( self, altname = None ):
+ resultingname = self.channel if self.count == 0 else '%s (%d)' % ( self.channel, self.count, )
+ li = xbmcgui.ListItem( label = resultingname if altname is None else altname )
+ xbmcplugin.addDirectoryItem(
+ handle = self.handle,
+ url = self.build_url( {
+ 'mode': self.next,
+ 'channel': self.id
+ } ),
+ listitem = li,
+ isFolder = True
+ )
+
+ def End( self ):
+ xbmcplugin.endOfDirectory( self.handle )
+
+ def build_url( self, query ):
+ return self.base_url + '?' + urllib.urlencode( query )
diff --git a/plugin.video.mediathekview/classes/exceptions.py b/plugin.video.mediathekview/classes/exceptions.py
new file mode 100644
index 0000000..53d7f14
--- /dev/null
+++ b/plugin.video.mediathekview/classes/exceptions.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll
+#
+
+class DatabaseCorrupted( RuntimeError ):
+ """This exception is raised when the database throws errors during update"""
+
+class DatabaseLost( RuntimeError ):
+ """This exception is raised when the connection to the database is lost during update"""
diff --git a/plugin.video.mediathekview/classes/film.py b/plugin.video.mediathekview/classes/film.py
new file mode 100644
index 0000000..b417078
--- /dev/null
+++ b/plugin.video.mediathekview/classes/film.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+
+# -- Classes ------------------------------------------------
+class Film( object ):
+ def __init__( self ):
+ self.id = 0
+ self.title = u''
+ self.show = u''
+ self.channel = u''
+ self.description = u''
+ self.seconds = 0
+ self.size = 0
+ self.aired = u''
+ self.url_sub = u''
+ self.url_video = u''
+ self.url_video_sd = u''
+ self.url_video_hd = u''
diff --git a/plugin.video.mediathekview/classes/filmui.py b/plugin.video.mediathekview/classes/filmui.py
new file mode 100644
index 0000000..eb2cfa8
--- /dev/null
+++ b/plugin.video.mediathekview/classes/filmui.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import xbmcaddon, xbmcplugin, xbmcgui
+
+from classes.film import Film
+from classes.settings import Settings
+
+# -- Classes ------------------------------------------------
+class FilmUI( Film ):
+ def __init__( self, plugin, sortmethods = [ xbmcplugin.SORT_METHOD_TITLE, xbmcplugin.SORT_METHOD_DATE, xbmcplugin.SORT_METHOD_DURATION, xbmcplugin.SORT_METHOD_SIZE ] ):
+ self.plugin = plugin
+ self.handle = plugin.addon_handle
+ self.settings = Settings()
+ self.sortmethods = sortmethods
+ self.showshows = False
+ self.showchannels = False
+
+ def Begin( self, showshows, showchannels ):
+ self.showshows = showshows
+ self.showchannels = showchannels
+ # xbmcplugin.setContent( self.handle, 'tvshows' )
+ for method in self.sortmethods:
+ xbmcplugin.addSortMethod( self.handle, method )
+
+ def Add( self, alttitle = None ):
+ # get the best url
+ videourl = self.url_video_hd if ( self.url_video_hd != "" and self.settings.preferhd ) else self.url_video if self.url_video != "" else self.url_video_sd
+ videohds = " (HD)" if ( self.url_video_hd != "" and self.settings.preferhd ) else ""
+ # exit if no url supplied
+ if videourl == "":
+ return
+
+ if alttitle is not None:
+ resultingtitle = alttitle
+ else:
+ if self.showshows:
+ resultingtitle = self.show + ': ' + self.title
+ else:
+ resultingtitle = self.title
+ if self.showchannels:
+ resultingtitle += ' [' + self.channel + ']'
+
+ infoLabels = {
+ 'title' : resultingtitle + videohds,
+ 'sorttitle' : resultingtitle,
+ 'tvshowtitle' : self.show,
+ 'plot' : self.description
+ }
+
+ if self.size > 0:
+ infoLabels['size'] = self.size * 1024 * 1024
+
+ if self.seconds > 0:
+ infoLabels['duration'] = self.seconds
+
+ if self.aired is not None:
+ airedstring = '%s' % self.aired
+ infoLabels['date'] = airedstring[8:10] + '-' + airedstring[5:7] + '-' + airedstring[:4]
+ infoLabels['aired'] = airedstring
+ infoLabels['dateadded'] = airedstring
+
+ li = xbmcgui.ListItem( resultingtitle, self.description )
+ li.setInfo( type = 'video', infoLabels = infoLabels )
+ li.setProperty( 'IsPlayable', 'true' )
+
+ # create context menu
+ contextmenu = []
+ if self.size > 0:
+ # Download video
+ contextmenu.append( (
+ self.plugin.language( 30921 ),
+ 'RunPlugin({})'.format( self.plugin.build_url( { 'mode': "download", 'id': self.id, 'quality': 1 } ) )
+ ) )
+ if self.url_video_hd:
+ # Download SD video
+ contextmenu.append( (
+ self.plugin.language( 30923 ),
+ 'RunPlugin({})'.format( self.plugin.build_url( { 'mode': "download", 'id': self.id, 'quality': 2 } ) )
+ ) )
+ if self.url_video_sd:
+ # Download SD video
+ contextmenu.append( (
+ self.plugin.language( 30922 ),
+ 'RunPlugin({})'.format( self.plugin.build_url( { 'mode': "download", 'id': self.id, 'quality': 0 } ) )
+ ) )
+ # Add to queue
+ # TODO: Enable later
+# contextmenu.append( (
+# self.plugin.language( 30924 ),
+# 'RunPlugin({})'.format( self.plugin.build_url( { 'mode': "enqueue", 'id': self.id } ) )
+# ) )
+ li.addContextMenuItems( contextmenu )
+
+ xbmcplugin.addDirectoryItem(
+ handle = self.handle,
+ url = videourl,
+ listitem = li,
+ isFolder = False
+ )
+
+ def End( self ):
+ xbmcplugin.endOfDirectory( self.handle, cacheToDisc = False )
diff --git a/plugin.video.mediathekview/classes/initialui.py b/plugin.video.mediathekview/classes/initialui.py
new file mode 100644
index 0000000..e9593d8
--- /dev/null
+++ b/plugin.video.mediathekview/classes/initialui.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import sys, urllib
+import xbmcplugin, xbmcgui
+
+from classes.settings import Settings
+
+# -- Classes ------------------------------------------------
+class InitialUI( object ):
+ def __init__( self, handle, sortmethods = [ xbmcplugin.SORT_METHOD_TITLE ] ):
+ self.handle = handle
+ self.sortmethods = sortmethods
+ self.channelid = 0
+ self.initial = ''
+ self.count = 0
+
+ def Begin( self, channelid ):
+ self.channelid = channelid
+ for method in self.sortmethods:
+ xbmcplugin.addSortMethod( self.handle, method )
+
+ def Add( self, altname = None ):
+ if altname is None:
+ resultingname = '%s (%d)' % ( self.initial if self.initial != ' ' and self.initial != '' else ' No Title', self.count )
+ else:
+ resultingname = altname
+ li = xbmcgui.ListItem( label = resultingname )
+ xbmcplugin.addDirectoryItem(
+ handle = self.handle,
+ url = self.build_url( {
+ 'mode': "shows",
+ 'channel': self.channelid,
+ 'initial': self.initial,
+ 'count': self.count
+ } ),
+ listitem = li,
+ isFolder = True
+ )
+
+ def End( self ):
+ xbmcplugin.endOfDirectory( self.handle )
+
+ def build_url( self, query ):
+ return sys.argv[0] + '?' + urllib.urlencode( query )
diff --git a/plugin.video.mediathekview/classes/mvupdate.py b/plugin.video.mediathekview/classes/mvupdate.py
new file mode 100644
index 0000000..f314679
--- /dev/null
+++ b/plugin.video.mediathekview/classes/mvupdate.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2018, Leo Moll
+
+# -- Imports ------------------------------------------------
+import os
+import sys
+import argparse
+import datetime
+import xml.etree.ElementTree as ET
+
+from de.yeasoft.base.Logger import Logger
+from classes.store import Store
+from classes.updater import MediathekViewUpdater
+
+# -- Classes ------------------------------------------------
+class Settings( object ):
+ def __init__( self, args ):
+ self.datapath = args.path if args.dbtype == 'sqlite' else './'
+ self.type = { 'sqlite' : '0', 'mysql' : '1' }.get( args.dbtype, '0' )
+ if self.type == '1':
+ self.host = args.host
+ self.user = args.user
+ self.password = args.password
+ self.database = args.database
+ self.nofuture = True
+ self.minlength = 0
+ self.groupshows = False
+ self.updenabled = True
+ self.updinterval = 3600
+ self.updxzbin = ''
+
+class AppLogger( Logger ):
+
+ def __init__( self, name, version, topic = None, verbosity = 0 ):
+ super( AppLogger, self ).__init__( name, version, topic )
+ self.verbosity = verbosity
+
+ def getNewLogger( self, topic = None ):
+ return AppLogger( self.name, self.version, topic, self.verbosity )
+
+ def debug( self, message, *args ):
+ self._log( 2, message, *args )
+
+ def info( self, message, *args ):
+ self._log( 1, message, *args )
+
+ def warn( self, message, *args ):
+ self._log( 0, message, *args )
+
+ def error( self, message, *args ):
+ self._log( -1, message, *args )
+
+ def _log( self, level, message, *args ):
+ parts = []
+ for arg in args:
+ part = arg
+ if isinstance( arg, basestring ):
+ part = arg # arg.decode('utf-8')
+ parts.append( part )
+ output = '{} {} {}{}'.format(
+ datetime.datetime.now(),
+ { -1: 'ERROR', 0: 'WARNING', 1: 'NOTICE', 2: 'DEBUG' }.get( level, 2 ),
+ self.prefix,
+ message.format( *parts )
+ )
+
+ if level < 0:
+ # error
+ sys.stderr.write( output + '\n' )
+ sys.stderr.flush()
+ elif self.verbosity >= level:
+ # other message
+ print( output )
+
+class Notifier( object ):
+ def __init__( self ):
+ pass
+ def GetEnteredText( self, deftext = '', heading = '', hidden = False ):
+ pass
+ def ShowNotification( self, heading, message, icon = None, time = 5000, sound = True ):
+ pass
+ def ShowWarning( self, heading, message, time = 5000, sound = True ):
+ pass
+ def ShowError( self, heading, message, time = 5000, sound = True ):
+ pass
+ def ShowBGDialog( self, heading = None, message = None ):
+ pass
+ def UpdateBGDialog( self, percent, heading = None, message = None ):
+ pass
+ def CloseBGDialog( self ):
+ pass
+ def ShowDatabaseError( self, err ):
+ pass
+ def ShowDownloadError( self, name, err ):
+ pass
+ def ShowMissingXZError( self ):
+ pass
+ def ShowDownloadProgress( self ):
+ pass
+ def UpdateDownloadProgress( self, percent, message = None ):
+ pass
+ def CloseDownloadProgress( self ):
+ pass
+ def ShowUpdateProgress( self ):
+ pass
+ def UpdateUpdateProgress( self, percent, count, channels, shows, movies ):
+ pass
+ def CloseUpdateProgress( self ):
+ pass
+
+class MediathekViewMonitor( object ):
+ def abortRequested( self ):
+ return False
+
+class UpdateApp( AppLogger ):
+ def __init__( self ):
+ try:
+ self.mypath = os.path.dirname( sys.argv[0] )
+ tree = ET.parse( self.mypath + '/addon.xml' )
+ version = tree.getroot().attrib['version']
+ AppLogger.__init__( self, os.path.basename( sys.argv[0] ), version )
+ except Exception as err:
+ AppLogger.__init__( self, os.path.basename( sys.argv[0] ), '0.0' )
+
+ def Init( self ):
+ parser = argparse.ArgumentParser(
+ formatter_class = argparse.ArgumentDefaultsHelpFormatter,
+ description = 'This is the standalone database updater. It downloads the current database update from mediathekview.de and integrates it in a local database'
+ )
+ parser.add_argument(
+ '-v', '--verbose',
+ default = 0,
+ action = 'count',
+ help = 'Show progress messages'
+ )
+ subparsers = parser.add_subparsers(
+ dest = 'dbtype',
+ help = 'target database'
+ )
+ sqliteopts = subparsers.add_parser( 'sqlite', formatter_class = argparse.ArgumentDefaultsHelpFormatter )
+ sqliteopts.add_argument(
+ '-p', '--path',
+ dest = 'path',
+ help = 'alternative path for the sqlite database',
+ default = './'
+ )
+ mysqlopts = subparsers.add_parser( 'mysql', formatter_class = argparse.ArgumentDefaultsHelpFormatter )
+ mysqlopts.add_argument(
+ '-H', '--host',
+ dest = 'host',
+ help = 'hostname or ip of the MySQL server',
+ default = 'localhost'
+ )
+ mysqlopts.add_argument(
+ '-u', '--user',
+ dest = 'user',
+ help = 'username for the MySQL server connection',
+ default = 'filmliste'
+ )
+ mysqlopts.add_argument(
+ '-p', '--password',
+ dest = 'password',
+ help = 'password for the MySQL server connection',
+ default = None
+ )
+ mysqlopts.add_argument(
+ '-d', '--database',
+ dest = 'database',
+ default = 'filmliste',
+ help = 'MySQL database for mediathekview'
+ )
+ self.args = parser.parse_args()
+ self.verbosity = self.args.verbose
+
+ self.info( 'Startup' )
+ self.settings = Settings( self.args )
+ self.notifier = Notifier()
+ self.monitor = MediathekViewMonitor()
+ self.updater = MediathekViewUpdater( self.getNewLogger( 'MediathekViewUpdater' ), self.notifier, self.settings, self.monitor )
+ if self.updater.PrerequisitesMissing():
+ self.error( 'Prerequisites are missing' )
+ self.Exit()
+ exit( 1 )
+ self.updater.Init()
+
+ def Run( self ):
+ self.info( 'Starting up...' )
+ updateop = self.updater.GetCurrentUpdateOperation()
+ if updateop == 1:
+ # full update
+ self.info( 'Initiating full update...' )
+ self.updater.Update( True )
+ elif updateop == 2:
+ # differential update
+ self.info( 'Initiating differential update...' )
+ self.updater.Update( False )
+ self.info( 'Exiting...' )
+
+ def Exit( self ):
+ self.updater.Exit()
diff --git a/plugin.video.mediathekview/classes/notifier.py b/plugin.video.mediathekview/classes/notifier.py
new file mode 100644
index 0000000..f76d059
--- /dev/null
+++ b/plugin.video.mediathekview/classes/notifier.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import xbmcaddon, xbmcplugin
+
+from de.yeasoft.kodi.KodiUI import KodiUI
+
+# -- Classes ------------------------------------------------
+class Notifier( KodiUI ):
+ def __init__( self ):
+ super( Notifier, self ).__init__()
+ self.language = xbmcaddon.Addon().getLocalizedString
+
+ def ShowDatabaseError( self, err ):
+ self.ShowError( self.language( 30951 ), '{}'.format( err ) )
+
+ def ShowDownloadError( self, name, err ):
+ self.ShowError( self.language( 30952 ), self.language( 30953 ).format( name, err ) )
+
+ def ShowMissingXZError( self ):
+ self.ShowError( self.language( 30952 ), self.language( 30954 ), time = 10000 )
+
+ def ShowDownloadProgress( self ):
+ self.ShowBGDialog( self.language( 30955 ) )
+
+ def UpdateDownloadProgress( self, percent, message = None ):
+ self.UpdateBGDialog( percent, message = message )
+
+ def CloseDownloadProgress( self ):
+ self.CloseBGDialog()
+
+ def ShowUpdateProgress( self ):
+ self.ShowBGDialog( self.language( 30956 ) )
+
+ def UpdateUpdateProgress( self, percent, count, channels, shows, movies ):
+ message = self.language( 30957 ) % ( count, channels, shows, movies )
+ self.UpdateBGDialog( percent, message = message )
+
+ def CloseUpdateProgress( self ):
+ self.CloseBGDialog()
diff --git a/plugin.video.mediathekview/classes/settings.py b/plugin.video.mediathekview/classes/settings.py
new file mode 100644
index 0000000..58ea805
--- /dev/null
+++ b/plugin.video.mediathekview/classes/settings.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import os
+import xbmc,xbmcaddon
+
+# -- Classes ------------------------------------------------
+class Settings( object ):
+ def __init__( self ):
+ self.addon = xbmcaddon.Addon()
+ self.Reload()
+
+ def Reload( self ):
+ self.datapath = os.path.join( xbmc.translatePath( "special://masterprofile" ).decode('utf-8'), 'addon_data', self.addon.getAddonInfo( 'id' ).decode('utf-8') )
+ self.firstrun = self.addon.getSetting( 'firstrun' ) == 'true'
+ self.preferhd = self.addon.getSetting( 'quality' ) == 'true'
+ self.nofuture = self.addon.getSetting( 'nofuture' ) == 'true'
+ self.minlength = int( float( self.addon.getSetting( 'minlength' ) ) ) * 60
+ self.groupshows = self.addon.getSetting( 'groupshows' ) == 'true'
+ self.downloadpath = self.addon.getSetting( 'downloadpath' )
+ self.type = self.addon.getSetting( 'dbtype' )
+ self.host = self.addon.getSetting( 'dbhost' )
+ self.user = self.addon.getSetting( 'dbuser' )
+ self.password = self.addon.getSetting( 'dbpass' )
+ self.database = self.addon.getSetting( 'dbdata' )
+ self.updenabled = self.addon.getSetting( 'updenabled' ) == 'true'
+ self.updinterval = int( float( self.addon.getSetting( 'updinterval' ) ) ) * 3600
+ self.updxzbin = self.addon.getSetting( 'updxzbin' )
+
+ def HandleFirstRun( self ):
+ if self.firstrun:
+ self.firstrun = False
+ self.addon.setSetting( 'firstrun', 'false' )
+ return True
+ return False
diff --git a/plugin.video.mediathekview/classes/show.py b/plugin.video.mediathekview/classes/show.py
new file mode 100644
index 0000000..e0c9127
--- /dev/null
+++ b/plugin.video.mediathekview/classes/show.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+
+# -- Classes ------------------------------------------------
+class Show( object ):
+ def __init__( self ):
+ self.id = 0
+ self.channelid = 0
+ self.show = ''
+ self.channel = '' \ No newline at end of file
diff --git a/plugin.video.mediathekview/classes/showui.py b/plugin.video.mediathekview/classes/showui.py
new file mode 100644
index 0000000..b6f0bf8
--- /dev/null
+++ b/plugin.video.mediathekview/classes/showui.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import sys, urllib
+import xbmcplugin, xbmcgui
+
+from classes.show import Show
+from classes.settings import Settings
+
+# -- Classes ------------------------------------------------
+class ShowUI( Show ):
+ def __init__( self, handle, sortmethods = [ xbmcplugin.SORT_METHOD_TITLE ] ):
+ self.base_url = sys.argv[0]
+ self.handle = handle
+ self.sortmethods = sortmethods
+ self.querychannelid = 0
+
+ def Begin( self, channelid ):
+ self.querychannelid = channelid
+ for method in self.sortmethods:
+ xbmcplugin.addSortMethod( self.handle, method )
+
+ def Add( self, altname = None ):
+ if altname is not None:
+ resultingname = altname
+ elif self.querychannelid == '0':
+ resultingname = self.show + ' [' + self.channel + ']'
+ else:
+ resultingname = self.show
+ li = xbmcgui.ListItem( label = resultingname )
+ xbmcplugin.addDirectoryItem(
+ handle = self.handle,
+ url = self.build_url( {
+ 'mode': "films",
+ 'show': self.id
+ } ),
+ listitem = li,
+ isFolder = True
+ )
+
+ def End( self ):
+ xbmcplugin.endOfDirectory( self.handle )
+
+ def build_url( self, query ):
+ return self.base_url + '?' + urllib.urlencode( query )
diff --git a/plugin.video.mediathekview/classes/store.py b/plugin.video.mediathekview/classes/store.py
new file mode 100644
index 0000000..15c6ef2
--- /dev/null
+++ b/plugin.video.mediathekview/classes/store.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll
+#
+
+# -- Imports ------------------------------------------------
+from classes.storemysql import StoreMySQL
+from classes.storesqlite import StoreSQLite
+
+# -- Classes ------------------------------------------------
+class Store( object ):
+ def __init__( self, logger, notifier, settings ):
+ self.logger = logger
+ self.notifier = notifier
+ self.settings = settings
+ # load storage engine
+ if settings.type == '0':
+ self.logger.info( 'Database driver: Internal (sqlite)' )
+ self.db = StoreSQLite( logger.getNewLogger( 'StoreMySQL' ), notifier, self.settings )
+ elif settings.type == '1':
+ self.logger.info( 'Database driver: External (mysql)' )
+ self.db = StoreMySQL( logger.getNewLogger( 'StoreMySQL' ), notifier, self.settings )
+ else:
+ self.logger.warn( 'Unknown Database driver selected' )
+ self.db = None
+
+ def __del__( self ):
+ if self.db is not None:
+ del self.db
+ self.db = None
+
+ def Init( self, reset = False ):
+ if self.db is not None:
+ self.db.Init( reset )
+
+ def Exit( self ):
+ if self.db is not None:
+ self.db.Exit()
+
+ def Search( self, search, filmui ):
+ if self.db is not None:
+ self.db.Search( search, filmui )
+
+ def SearchFull( self, search, filmui ):
+ if self.db is not None:
+ self.db.SearchFull( search, filmui )
+
+ def GetRecents( self, channelid, filmui ):
+ if self.db is not None:
+ self.db.GetRecents( channelid, filmui )
+
+ def GetLiveStreams( self, filmui ):
+ if self.db is not None:
+ self.db.GetLiveStreams( filmui )
+
+ def GetChannels( self, channelui ):
+ if self.db is not None:
+ self.db.GetChannels( channelui )
+
+ def GetRecentChannels( self, channelui ):
+ if self.db is not None:
+ self.db.GetRecentChannels( channelui )
+
+ def GetInitials( self, channelid, initialui ):
+ if self.db is not None:
+ self.db.GetInitials( channelid, initialui )
+
+ def GetShows( self, channelid, initial, showui ):
+ if self.db is not None:
+ self.db.GetShows( channelid, initial, showui )
+
+ def GetFilms( self, showid, filmui ):
+ if self.db is not None:
+ self.db.GetFilms( showid, filmui )
+
+ def RetrieveFilmInfo( self, filmid ):
+ if self.db is not None:
+ return self.db.RetrieveFilmInfo( filmid )
+
+ def GetStatus( self ):
+ if self.db is not None:
+ return self.db.GetStatus()
+ else:
+ return {
+ 'modified': int( time.time() ),
+ 'status': 'UNINIT',
+ 'lastupdate': 0,
+ 'filmupdate': 0,
+ 'fullupdate': 0,
+ 'add_chn': 0,
+ 'add_shw': 0,
+ 'add_mov': 0,
+ 'del_chn': 0,
+ 'del_shw': 0,
+ 'del_mov': 0,
+ 'tot_chn': 0,
+ 'tot_shw': 0,
+ 'tot_mov': 0
+ }
+
+ def UpdateStatus( self, status = None, lastupdate = None, filmupdate = None, fullupdate = None, add_chn = None, add_shw = None, add_mov = None, del_chn = None, del_shw = None, del_mov = None, tot_chn = None, tot_shw = None, tot_mov = None ):
+ if self.db is not None:
+ self.db.UpdateStatus( status, lastupdate, filmupdate, fullupdate, add_chn, add_shw, add_mov, del_chn, del_shw, del_mov, tot_chn, tot_shw, tot_mov )
+
+ def SupportsUpdate( self ):
+ if self.db is not None:
+ return self.db.SupportsUpdate()
+ return False
+
+ def ftInit( self ):
+ if self.db is not None:
+ return self.db.ftInit()
+ return False
+
+ def ftUpdateStart( self, full ):
+ if self.db is not None:
+ return self.db.ftUpdateStart( full )
+ return ( 0, 0, 0, )
+
+ def ftUpdateEnd( self, delete ):
+ if self.db is not None:
+ return self.db.ftUpdateEnd( delete )
+ return ( 0, 0, 0, 0, 0, 0, )
+
+ def ftInsertFilm( self, film, commit = True ):
+ if self.db is not None:
+ return self.db.ftInsertFilm( film, commit )
+ return ( 0, 0, 0, 0, )
diff --git a/plugin.video.mediathekview/classes/storemysql.py b/plugin.video.mediathekview/classes/storemysql.py
new file mode 100644
index 0000000..a272277
--- /dev/null
+++ b/plugin.video.mediathekview/classes/storemysql.py
@@ -0,0 +1,534 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll
+#
+
+# -- Imports ------------------------------------------------
+import string, time
+import mysql.connector
+
+from classes.film import Film
+from classes.exceptions import DatabaseCorrupted
+
+# -- Classes ------------------------------------------------
+class StoreMySQL( object ):
+ def __init__( self, logger, notifier, settings ):
+ self.conn = None
+ self.logger = logger
+ self.notifier = notifier
+ self.settings = settings
+ # useful query fragments
+ self.sql_query_films = "SELECT film.id,`title`,`show`,`channel`,`description`,TIME_TO_SEC(`duration`) AS `seconds`,`size`,`aired`,`url_sub`,`url_video`,`url_video_sd`,`url_video_hd` FROM `film` LEFT JOIN `show` ON show.id=film.showid LEFT JOIN `channel` ON channel.id=film.channelid"
+ self.sql_cond_recent = "( TIMESTAMPDIFF(HOUR,`aired`,CURRENT_TIMESTAMP()) < 24 )"
+ self.sql_cond_nofuture = " AND ( ( `aired` IS NULL ) OR ( TIMESTAMPDIFF(HOUR,`aired`,CURRENT_TIMESTAMP()) > 0 ) )" if settings.nofuture else ""
+ self.sql_cond_minlength = " AND ( ( `duration` IS NULL ) OR ( TIME_TO_SEC(`duration`) >= %d ) )" % settings.minlength if settings.minlength > 0 else ""
+
+ def Init( self, reset = False ):
+ self.logger.info( 'Using MySQL connector version {}', mysql.connector.__version__ )
+ try:
+ self.conn = mysql.connector.connect(
+ host = self.settings.host,
+ user = self.settings.user,
+ password = self.settings.password,
+ database = self.settings.database
+ )
+ except mysql.connector.Error as err:
+ self.conn = None
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def Exit( self ):
+ if self.conn is not None:
+ self.conn.close()
+
+ def Search( self, search, filmui ):
+ self._Search_Condition( '( ( `title` LIKE "%%%s%%" ) OR ( `show` LIKE "%%%s%%" ) )' % ( search, search, ), filmui, True, True )
+
+ def SearchFull( self, search, filmui ):
+ self._Search_Condition( '( ( `title` LIKE "%%%s%%" ) OR ( `show` LIKE "%%%s%%" ) ) OR ( `description` LIKE "%%%s%%") )' % ( search, search, search ), filmui, True, True )
+
+ def GetRecents( self, channelid, filmui ):
+ sql_cond_channel = ' AND ( film.channelid=' + str( channelid ) + ' ) ' if channelid != '0' else ''
+ self._Search_Condition( self.sql_cond_recent + sql_cond_channel, filmui, True, False )
+
+ def GetLiveStreams( self, filmui ):
+ self._Search_Condition( '( show.search="LIVESTREAM" )', filmui, False, False )
+
+ def GetChannels( self, channelui ):
+ self._Channels_Condition( None, channelui )
+
+ def GetRecentChannels( self, channelui ):
+ self._Channels_Condition( self.sql_cond_recent, channelui )
+
+ def _Channels_Condition( self, condition, channelui):
+ if self.conn is None:
+ return
+ try:
+ if condition is None:
+ query = 'SELECT `id`,`channel`,0 AS `count` FROM `channel`'
+ else:
+ query = 'SELECT channel.id AS `id`,`channel`,COUNT(*) AS `count` FROM `film` LEFT JOIN `channel` ON channel.id=film.channelid WHERE ' + condition + ' GROUP BY channel.id'
+ self.logger.info( 'MySQL Query: {}', query )
+ cursor = self.conn.cursor()
+ cursor.execute( query )
+ channelui.Begin()
+ for ( channelui.id, channelui.channel, channelui.count ) in cursor:
+ channelui.Add()
+ channelui.End()
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetInitials( self, channelid, initialui ):
+ if self.conn is None:
+ return
+ try:
+ condition = 'WHERE ( `channelid`=' + str( channelid ) + ' ) ' if channelid != '0' else ''
+ self.logger.info( 'MySQL Query: {}',
+ 'SELECT LEFT(`search`,1) AS letter,COUNT(*) AS `count` FROM `show` ' +
+ condition +
+ 'GROUP BY LEFT(search,1)'
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ 'SELECT LEFT(`search`,1) AS letter,COUNT(*) AS `count` FROM `show` ' +
+ condition +
+ 'GROUP BY LEFT(`search`,1)'
+ )
+ initialui.Begin( channelid )
+ for ( initialui.initial, initialui.count ) in cursor:
+ initialui.Add()
+ initialui.End()
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetShows( self, channelid, initial, showui ):
+ if self.conn is None:
+ return
+ try:
+ if channelid == '0' and self.settings.groupshows:
+ query = 'SELECT GROUP_CONCAT(show.id),GROUP_CONCAT(`channelid`),`show`,GROUP_CONCAT(`channel`) FROM `show` LEFT JOIN `channel` ON channel.id=show.channelid WHERE ( `show` LIKE "%s%%" ) GROUP BY `show`' % initial
+ elif channelid == '0':
+ query = 'SELECT show.id,show.channelid,show.show,channel.channel FROM `show` LEFT JOIN channel ON channel.id=show.channelid WHERE ( `show` LIKE "%s%%" )' % initial
+ else:
+ query = 'SELECT show.id,show.channelid,show.show,channel.channel FROM `show` LEFT JOIN channel ON channel.id=show.channelid WHERE ( `channelid`=%s ) AND ( `show` LIKE "%s%%" )' % ( channelid, initial )
+ self.logger.info( 'MySQL Query: {}', query )
+ cursor = self.conn.cursor()
+ cursor.execute( query )
+ showui.Begin( channelid )
+ for ( showui.id, showui.channelid, showui.show, showui.channel ) in cursor:
+ showui.Add()
+ showui.End()
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetFilms( self, showid, filmui ):
+ if self.conn is None:
+ return
+ if showid.find( ',' ) == -1:
+ # only one channel id
+ condition = '( `showid=`%s )' % showid
+ showchannels = False
+ else:
+ # multiple channel ids
+ condition = '( `showid` IN ( %s ) )' % showid
+ showchannels = True
+ self._Search_Condition( condition, filmui, False, showchannels )
+
+ def _Search_Condition( self, condition, filmui, showshows, showchannels ):
+ if self.conn is None:
+ return
+ try:
+ self.logger.info( 'MySQL Query: {}',
+ self.sql_query_films +
+ ' WHERE ' +
+ condition +
+ self.sql_cond_nofuture +
+ self.sql_cond_minlength
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ self.sql_query_films +
+ ' WHERE ' +
+ condition +
+ self.sql_cond_nofuture +
+ self.sql_cond_minlength
+ )
+ filmui.Begin( showshows, showchannels )
+ for ( filmui.id, filmui.title, filmui.show, filmui.channel, filmui.description, filmui.seconds, filmui.size, filmui.aired, filmui.url_sub, filmui.url_video, filmui.url_video_sd, filmui.url_video_hd ) in cursor:
+ filmui.Add()
+ filmui.End()
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def RetrieveFilmInfo( self, filmid ):
+ if self.conn is None:
+ return None
+ try:
+ condition = '( film.id={} )'.format( filmid )
+ self.logger.info( 'MySQL Query: {}',
+ self.sql_query_films +
+ ' WHERE ' +
+ condition
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ self.sql_query_films +
+ ' WHERE ' +
+ condition
+ )
+ film = Film()
+ for ( film.id, film.title, film.show, film.channel, film.description, film.seconds, film.size, film.aired, film.url_sub, film.url_video, film.url_video_sd, film.url_video_hd ) in cursor:
+ cursor.close()
+ return film
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return None
+
+ def GetStatus( self ):
+ status = {
+ 'modified': int( time.time() ),
+ 'status': '',
+ 'lastupdate': 0,
+ 'filmupdate': 0,
+ 'fullupdate': 0,
+ 'add_chn': 0,
+ 'add_shw': 0,
+ 'add_mov': 0,
+ 'del_chn': 0,
+ 'del_shw': 0,
+ 'del_mov': 0,
+ 'tot_chn': 0,
+ 'tot_shw': 0,
+ 'tot_mov': 0
+ }
+ if self.conn is None:
+ status['status'] = "UNINIT"
+ return status
+ try:
+ cursor = self.conn.cursor()
+ cursor.execute( 'SELECT * FROM `status` LIMIT 1' )
+ r = cursor.fetchall()
+ cursor.close()
+ self.conn.commit()
+ if len( r ) == 0:
+ status['status'] = "NONE"
+ return status
+ status['modified'] = r[0][0]
+ status['status'] = r[0][1]
+ status['lastupdate'] = r[0][2]
+ status['filmupdate'] = r[0][3]
+ status['fullupdate'] = r[0][4]
+ status['add_chn'] = r[0][5]
+ status['add_shw'] = r[0][6]
+ status['add_mov'] = r[0][7]
+ status['del_chn'] = r[0][8]
+ status['del_shw'] = r[0][9]
+ status['del_mov'] = r[0][10]
+ status['tot_chn'] = r[0][11]
+ status['tot_shw'] = r[0][12]
+ status['tot_mov'] = r[0][13]
+ return status
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ status['status'] = "UNINIT"
+ return status
+
+ def UpdateStatus( self, status = None, lastupdate = None, filmupdate = None, fullupdate = None, add_chn = None, add_shw = None, add_mov = None, del_chn = None, del_shw = None, del_mov = None, tot_chn = None, tot_shw = None, tot_mov = None ):
+ if self.conn is None:
+ return
+ new = self.GetStatus()
+ old = new['status']
+ if status is not None:
+ new['status'] = status
+ if lastupdate is not None:
+ new['lastupdate'] = lastupdate
+ if filmupdate is not None:
+ new['filmupdate'] = filmupdate
+ if fullupdate is not None:
+ new['fullupdate'] = fullupdate
+ if add_chn is not None:
+ new['add_chn'] = add_chn
+ if add_shw is not None:
+ new['add_shw'] = add_shw
+ if add_mov is not None:
+ new['add_mov'] = add_mov
+ if del_chn is not None:
+ new['del_chn'] = del_chn
+ if del_shw is not None:
+ new['del_shw'] = del_shw
+ if del_mov is not None:
+ new['del_mov'] = del_mov
+ if tot_chn is not None:
+ new['tot_chn'] = tot_chn
+ if tot_shw is not None:
+ new['tot_shw'] = tot_shw
+ if tot_mov is not None:
+ new['tot_mov'] = tot_mov
+ # TODO: we should only write, if we have changed something...
+ new['modified'] = int( time.time() )
+ try:
+ cursor = self.conn.cursor()
+ if old == "NONE":
+ # insert status
+ cursor.execute(
+ """
+ INSERT INTO `status` (
+ `modified`,
+ `status`,
+ `lastupdate`,
+ `filmupdate`,
+ `fullupdate`,
+ `add_chn`,
+ `add_shw`,
+ `add_mov`,
+ `del_chm`,
+ `del_shw`,
+ `del_mov`,
+ `tot_chn`,
+ `tot_shw`,
+ `tot_mov`
+ )
+ VALUES (
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s
+ )
+ """, (
+ new['modified'],
+ new['status'],
+ new['lastupdate'],
+ new['filmupdate'],
+ new['fullupdate'],
+ new['add_chn'],
+ new['add_shw'],
+ new['add_mov'],
+ new['del_chn'],
+ new['del_shw'],
+ new['del_mov'],
+ new['tot_chn'],
+ new['tot_shw'],
+ new['tot_mov'],
+ )
+ )
+ else:
+ # update status
+ cursor.execute(
+ """
+ UPDATE `status`
+ SET `modified` = %s,
+ `status` = %s,
+ `lastupdate` = %s,
+ `filmupdate` = %s,
+ `fullupdate` = %s,
+ `add_chn` = %s,
+ `add_shw` = %s,
+ `add_mov` = %s,
+ `del_chm` = %s,
+ `del_shw` = %s,
+ `del_mov` = %s,
+ `tot_chn` = %s,
+ `tot_shw` = %s,
+ `tot_mov` = %s
+ """, (
+ new['modified'],
+ new['status'],
+ new['lastupdate'],
+ new['filmupdate'],
+ new['fullupdate'],
+ new['add_chn'],
+ new['add_shw'],
+ new['add_mov'],
+ new['del_chn'],
+ new['del_shw'],
+ new['del_mov'],
+ new['tot_chn'],
+ new['tot_shw'],
+ new['tot_mov'],
+ )
+ )
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def SupportsUpdate( self ):
+ return True
+
+ def ftInit( self ):
+ # prevent concurrent updating
+ cursor = self.conn.cursor()
+ cursor.execute(
+ """
+ UPDATE `status`
+ SET `modified` = %s,
+ `status` = 'UPDATING'
+ WHERE ( `status` != 'UPDATING' )
+ OR
+ ( `modified` < %s )
+ """, (
+ int( time.time() ),
+ int( time.time() ) - 86400
+ )
+ )
+ retval = cursor.rowcount > 0
+ self.conn.commit()
+ cursor.close()
+ self.ft_channel = None
+ self.ft_channelid = None
+ self.ft_show = None
+ self.ft_showid = None
+ return retval
+
+ def ftUpdateStart( self, full ):
+ param = ( 1, ) if full else ( 0, )
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftUpdateStart', param )
+ for result in cursor.stored_results():
+ for ( cnt_chn, cnt_shw, cnt_mov ) in result:
+ cursor.close()
+ self.conn.commit()
+ return ( cnt_chn, cnt_shw, cnt_mov )
+ # should never happen
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, 0, )
+
+ def ftUpdateEnd( self, delete ):
+ param = ( 1, ) if delete else ( 0, )
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftUpdateEnd', param )
+ for result in cursor.stored_results():
+ for ( del_chn, del_shw, del_mov, cnt_chn, cnt_shw, cnt_mov ) in result:
+ cursor.close()
+ self.conn.commit()
+ return ( del_chn, del_shw, del_mov, cnt_chn, cnt_shw, cnt_mov )
+ # should never happen
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, 0, 0, 0, 0, )
+
+ def ftInsertFilm( self, film, commit ):
+ newchn = False
+ inschn = 0
+ insshw = 0
+ insmov = 0
+
+ # handle channel
+ if self.ft_channel != film['channel']:
+ # process changed channel
+ newchn = True
+ self.ft_channel = film['channel']
+ ( self.ft_channelid, inschn ) = self._insert_channel( self.ft_channel )
+ if self.ft_channelid == 0:
+ self.logger.info( 'Undefined error adding channel "{}"', self.ft_channel )
+ return ( 0, 0, 0, 0, )
+
+ if newchn or self.ft_show != film['show']:
+ # process changed show
+ self.ft_show = film['show']
+ ( self.ft_showid, insshw ) = self._insert_show( self.ft_channelid, self.ft_show, self._make_search( self.ft_show ) )
+ if self.ft_showid == 0:
+ self.logger.info( 'Undefined error adding show "{}"', self.ft_show )
+ return ( 0, 0, 0, 0, )
+
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftInsertFilm', (
+ self.ft_channelid,
+ self.ft_showid,
+ film["title"],
+ self._make_search( film['title'] ),
+ film["aired"],
+ film["duration"],
+ film["size"],
+ film["description"],
+ film["website"],
+ film["url_sub"],
+ film["url_video"],
+ film["url_video_sd"],
+ film["url_video_hd"],
+ film["airedepoch"],
+ ) )
+ for result in cursor.stored_results():
+ for ( filmid, insmov ) in result:
+ cursor.close()
+ if commit:
+ self.conn.commit()
+ return ( filmid, inschn, insshw, insmov )
+ # should never happen
+ cursor.close()
+ if commit:
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, 0, 0, )
+
+ def _insert_channel( self, channel ):
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftInsertChannel', ( channel, ) )
+ for result in cursor.stored_results():
+ for ( id, added ) in result:
+ cursor.close()
+ self.conn.commit()
+ return ( id, added )
+ # should never happen
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, )
+
+ def _insert_show( self, channelid, show, search ):
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftInsertShow', ( channelid, show, search, ) )
+ for result in cursor.stored_results():
+ for ( id, added ) in result:
+ cursor.close()
+ self.conn.commit()
+ return ( id, added )
+ # should never happen
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, )
+
+ def _make_search( self, val ):
+ cset = string.letters + string.digits + ' _-#'
+ search = ''.join( [ c for c in val if c in cset ] )
+ return search.upper().strip()
diff --git a/plugin.video.mediathekview/classes/storesqlite.py b/plugin.video.mediathekview/classes/storesqlite.py
new file mode 100644
index 0000000..f73acdf
--- /dev/null
+++ b/plugin.video.mediathekview/classes/storesqlite.py
@@ -0,0 +1,766 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll
+#
+
+# -- Imports ------------------------------------------------
+import os, stat, string, time
+import sqlite3
+
+from classes.film import Film
+from classes.exceptions import DatabaseCorrupted
+
+# -- Classes ------------------------------------------------
+class StoreSQLite( object ):
+ def __init__( self, logger, notifier, settings ):
+ self.logger = logger
+ self.notifier = notifier
+ self.settings = settings
+ # internals
+ self.conn = None
+ self.dbfile = os.path.join( self.settings.datapath, 'filmliste-v1.db' )
+ # useful query fragments
+ self.sql_query_films = "SELECT film.id,title,show,channel,description,duration,size,datetime(aired, 'unixepoch', 'localtime'),url_sub,url_video,url_video_sd,url_video_hd FROM film LEFT JOIN show ON show.id=film.showid LEFT JOIN channel ON channel.id=film.channelid"
+ self.sql_cond_recent = "( ( UNIX_TIMESTAMP() - aired ) <= 86400 )"
+ self.sql_cond_nofuture = " AND ( ( aired IS NULL ) OR ( ( UNIX_TIMESTAMP() - aired ) > 0 ) )" if settings.nofuture else ""
+ self.sql_cond_minlength = " AND ( ( duration IS NULL ) OR ( duration >= %d ) )" % settings.minlength if settings.minlength > 0 else ""
+
+ def Init( self, reset = False ):
+ self.logger.info( 'Using SQLite version {}, python library sqlite3 version {}', sqlite3.sqlite_version, sqlite3.version )
+ if not self._dir_exists( self.settings.datapath ):
+ os.mkdir( self.settings.datapath )
+ if reset == True or not self._file_exists( self.dbfile ):
+ self.logger.info( '===== RESET: Database will be deleted and regenerated =====' )
+ self._file_remove( self.dbfile )
+ self.conn = sqlite3.connect( self.dbfile, timeout = 60 )
+ self._handle_database_initialization()
+ else:
+ try:
+ self.conn = sqlite3.connect( self.dbfile, timeout = 60 )
+ except sqlite3.DatabaseError as err:
+ self.logger.error( 'Errore while opening database. Trying to fully reset the Database...' )
+ self.Init( reset = True )
+
+ self.conn.execute( 'pragma journal_mode=off' ) # 3x speed-up, check mode 'WAL'
+ self.conn.execute( 'pragma synchronous=off' ) # that is a bit dangerous :-) but faaaast
+
+ self.conn.create_function( 'UNIX_TIMESTAMP', 0, UNIX_TIMESTAMP )
+ self.conn.create_aggregate( 'GROUP_CONCAT', 1, GROUP_CONCAT )
+
+ def Exit( self ):
+ if self.conn is not None:
+ self.conn.close()
+ self.conn = None
+
+ def Search( self, search, filmui ):
+ self._Search_Condition( '( ( title LIKE "%%%s%%" ) OR ( show LIKE "%%%s%%" ) )' % ( search, search ), filmui, True, True )
+
+ def SearchFull( self, search, filmui ):
+ self._Search_Condition( '( ( title LIKE "%%%s%%" ) OR ( show LIKE "%%%s%%" ) OR ( description LIKE "%%%s%%") )' % ( search, search, search ), filmui, True, True )
+
+ def GetRecents( self, channelid, filmui ):
+ sql_cond_channel = ' AND ( film.channelid=' + str( channelid ) + ' ) ' if channelid != '0' else ''
+ self._Search_Condition( self.sql_cond_recent + sql_cond_channel, filmui, True, False )
+
+ def GetLiveStreams( self, filmui ):
+ self._Search_Condition( '( show.search="LIVESTREAM" )', filmui, False, False )
+
+ def GetChannels( self, channelui ):
+ self._Channels_Condition( None, channelui )
+
+ def GetRecentChannels( self, channelui ):
+ self._Channels_Condition( self.sql_cond_recent, channelui )
+
+ def _Channels_Condition( self, condition, channelui):
+ if self.conn is None:
+ return
+ try:
+ if condition is None:
+ query = 'SELECT id,channel,0 AS `count` FROM channel'
+ else:
+ query = 'SELECT channel.id AS `id`,channel,COUNT(*) AS `count` FROM film LEFT JOIN channel ON channel.id=film.channelid WHERE ' + condition + ' GROUP BY channel'
+ self.logger.info( 'SQLite Query: {}', query )
+ cursor = self.conn.cursor()
+ cursor.execute( query )
+ channelui.Begin()
+ for ( channelui.id, channelui.channel, channelui.count ) in cursor:
+ channelui.Add()
+ channelui.End()
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetInitials( self, channelid, initialui ):
+ if self.conn is None:
+ return
+ try:
+ condition = 'WHERE ( channelid=' + str( channelid ) + ' ) ' if channelid != '0' else ''
+ self.logger.info( 'SQlite Query: {}',
+ 'SELECT SUBSTR(search,1,1),COUNT(*) FROM show ' +
+ condition +
+ 'GROUP BY LEFT(search,1)'
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ 'SELECT SUBSTR(search,1,1),COUNT(*) FROM show ' +
+ condition +
+ 'GROUP BY SUBSTR(search,1,1)'
+ )
+ initialui.Begin( channelid )
+ for ( initialui.initial, initialui.count ) in cursor:
+ initialui.Add()
+ initialui.End()
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetShows( self, channelid, initial, showui ):
+ if self.conn is None:
+ return
+ try:
+ if channelid == '0' and self.settings.groupshows:
+ query = 'SELECT GROUP_CONCAT(show.id),GROUP_CONCAT(channelid),show,GROUP_CONCAT(channel) FROM show LEFT JOIN channel ON channel.id=show.channelid WHERE ( show LIKE "%s%%" ) GROUP BY show' % initial
+ elif channelid == '0':
+ query = 'SELECT show.id,show.channelid,show.show,channel.channel FROM show LEFT JOIN channel ON channel.id=show.channelid WHERE ( show LIKE "%s%%" )' % initial
+ else:
+ query = 'SELECT show.id,show.channelid,show.show,channel.channel FROM show LEFT JOIN channel ON channel.id=show.channelid WHERE ( channelid=%s ) AND ( show LIKE "%s%%" )' % ( channelid, initial )
+ self.logger.info( 'SQLite Query: {}', query )
+ cursor = self.conn.cursor()
+ cursor.execute( query )
+ showui.Begin( channelid )
+ for ( showui.id, showui.channelid, showui.show, showui.channel ) in cursor:
+ showui.Add()
+ showui.End()
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetFilms( self, showid, filmui ):
+ if self.conn is None:
+ return
+ if showid.find( ',' ) == -1:
+ # only one channel id
+ condition = '( showid=%s )' % showid
+ showchannels = False
+ else:
+ # multiple channel ids
+ condition = '( showid IN ( %s ) )' % showid
+ showchannels = True
+ self._Search_Condition( condition, filmui, False, showchannels )
+
+ def _Search_Condition( self, condition, filmui, showshows, showchannels ):
+ if self.conn is None:
+ return
+ try:
+ self.logger.info( 'SQLite Query: {}',
+ self.sql_query_films +
+ ' WHERE ' +
+ condition +
+ self.sql_cond_nofuture +
+ self.sql_cond_minlength
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ self.sql_query_films +
+ ' WHERE ' +
+ condition +
+ self.sql_cond_nofuture +
+ self.sql_cond_minlength
+ )
+ filmui.Begin( showshows, showchannels )
+ for ( filmui.id, filmui.title, filmui.show, filmui.channel, filmui.description, filmui.seconds, filmui.size, filmui.aired, filmui.url_sub, filmui.url_video, filmui.url_video_sd, filmui.url_video_hd ) in cursor:
+ filmui.Add()
+ filmui.End()
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def RetrieveFilmInfo( self, filmid ):
+ if self.conn is None:
+ return None
+ try:
+ condition = '( film.id={} )'.format( filmid )
+ self.logger.info( 'SQLite Query: {}',
+ self.sql_query_films +
+ ' WHERE ' +
+ condition
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ self.sql_query_films +
+ ' WHERE ' +
+ condition
+ )
+ film = Film()
+ for ( film.id, film.title, film.show, film.channel, film.description, film.seconds, film.size, film.aired, film.url_sub, film.url_video, film.url_video_sd, film.url_video_hd ) in cursor:
+ cursor.close()
+ return film
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return None
+
+ def GetStatus( self ):
+ status = {
+ 'modified': int( time.time() ),
+ 'status': '',
+ 'lastupdate': 0,
+ 'filmupdate': 0,
+ 'fullupdate': 0,
+ 'add_chn': 0,
+ 'add_shw': 0,
+ 'add_mov': 0,
+ 'del_chn': 0,
+ 'del_shw': 0,
+ 'del_mov': 0,
+ 'tot_chn': 0,
+ 'tot_shw': 0,
+ 'tot_mov': 0
+ }
+ if self.conn is None:
+ status['status'] = "UNINIT"
+ return status
+ self.conn.commit()
+ cursor = self.conn.cursor()
+ cursor.execute( 'SELECT * FROM `status` LIMIT 1' )
+ r = cursor.fetchall()
+ cursor.close()
+ if len( r ) == 0:
+ status['status'] = "NONE"
+ return status
+ status['modified'] = r[0][0]
+ status['status'] = r[0][1]
+ status['lastupdate'] = r[0][2]
+ status['filmupdate'] = r[0][3]
+ status['fullupdate'] = r[0][4]
+ status['add_chn'] = r[0][5]
+ status['add_shw'] = r[0][6]
+ status['add_mov'] = r[0][7]
+ status['del_chn'] = r[0][8]
+ status['del_shw'] = r[0][9]
+ status['del_mov'] = r[0][10]
+ status['tot_chn'] = r[0][11]
+ status['tot_shw'] = r[0][12]
+ status['tot_mov'] = r[0][13]
+ return status
+
+ def UpdateStatus( self, status = None, lastupdate = None, filmupdate = None, fullupdate = None, add_chn = None, add_shw = None, add_mov = None, del_chn = None, del_shw = None, del_mov = None, tot_chn = None, tot_shw = None, tot_mov = None ):
+ if self.conn is None:
+ return
+ new = self.GetStatus()
+ old = new['status']
+ if status is not None:
+ new['status'] = status
+ if lastupdate is not None:
+ new['lastupdate'] = lastupdate
+ if filmupdate is not None:
+ new['filmupdate'] = filmupdate
+ if fullupdate is not None:
+ new['fullupdate'] = fullupdate
+ if add_chn is not None:
+ new['add_chn'] = add_chn
+ if add_shw is not None:
+ new['add_shw'] = add_shw
+ if add_mov is not None:
+ new['add_mov'] = add_mov
+ if del_chn is not None:
+ new['del_chn'] = del_chn
+ if del_shw is not None:
+ new['del_shw'] = del_shw
+ if del_mov is not None:
+ new['del_mov'] = del_mov
+ if tot_chn is not None:
+ new['tot_chn'] = tot_chn
+ if tot_shw is not None:
+ new['tot_shw'] = tot_shw
+ if tot_mov is not None:
+ new['tot_mov'] = tot_mov
+ # TODO: we should only write, if we have changed something...
+ new['modified'] = int( time.time() )
+ cursor = self.conn.cursor()
+ if old == "NONE":
+ # insert status
+ cursor.execute(
+ """
+ INSERT INTO `status` (
+ `modified`,
+ `status`,
+ `lastupdate`,
+ `filmupdate`,
+ `fullupdate`,
+ `add_chn`,
+ `add_shw`,
+ `add_mov`,
+ `del_chm`,
+ `del_shw`,
+ `del_mov`,
+ `tot_chn`,
+ `tot_shw`,
+ `tot_mov`
+ )
+ VALUES (
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?
+ )
+ """, (
+ new['modified'],
+ new['status'],
+ new['lastupdate'],
+ new['filmupdate'],
+ new['fullupdate'],
+ new['add_chn'],
+ new['add_shw'],
+ new['add_mov'],
+ new['del_chn'],
+ new['del_shw'],
+ new['del_mov'],
+ new['tot_chn'],
+ new['tot_shw'],
+ new['tot_mov'],
+ )
+ )
+ else:
+ # update status
+ cursor.execute(
+ """
+ UPDATE `status`
+ SET `modified` = ?,
+ `status` = ?,
+ `lastupdate` = ?,
+ `filmupdate` = ?,
+ `fullupdate` = ?,
+ `add_chn` = ?,
+ `add_shw` = ?,
+ `add_mov` = ?,
+ `del_chm` = ?,
+ `del_shw` = ?,
+ `del_mov` = ?,
+ `tot_chn` = ?,
+ `tot_shw` = ?,
+ `tot_mov` = ?
+ """, (
+ new['modified'],
+ new['status'],
+ new['lastupdate'],
+ new['filmupdate'],
+ new['fullupdate'],
+ new['add_chn'],
+ new['add_shw'],
+ new['add_mov'],
+ new['del_chn'],
+ new['del_shw'],
+ new['del_mov'],
+ new['tot_chn'],
+ new['tot_shw'],
+ new['tot_mov'],
+ )
+ )
+ cursor.close()
+ self.conn.commit()
+
+ def SupportsUpdate( self ):
+ return True
+
+ def ftInit( self ):
+ try:
+ # prevent concurrent updating
+ self.conn.commit()
+ cursor = self.conn.cursor()
+ cursor.execute(
+ """
+ UPDATE `status`
+ SET `modified` = ?,
+ `status` = 'UPDATING'
+ WHERE ( `status` != 'UPDATING' )
+ OR
+ ( `modified` < ? )
+ """, (
+ int( time.time() ),
+ int( time.time() ) - 86400
+ )
+ )
+ retval = cursor.rowcount > 0
+ self.conn.commit()
+ cursor.close()
+ self.ft_channel = None
+ self.ft_channelid = None
+ self.ft_show = None
+ self.ft_showid = None
+ return retval
+ except sqlite3.DatabaseError as err:
+ self._handle_database_corruption( err )
+ raise DatabaseCorrupted( 'Database error during critical operation: {} - Database will be rebuilt from scratch.'.format( err ) )
+
+ def ftUpdateStart( self, full ):
+ try:
+ cursor = self.conn.cursor()
+ if full:
+ cursor.executescript( """
+ UPDATE `channel`
+ SET `touched` = 0;
+
+ UPDATE `show`
+ SET `touched` = 0;
+
+ UPDATE `film`
+ SET `touched` = 0;
+ """ )
+ cursor.execute( 'SELECT COUNT(*) FROM `channel`' )
+ r1 = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `show`' )
+ r2 = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `film`' )
+ r3 = cursor.fetchone()
+ cursor.close()
+ self.conn.commit()
+ return ( r1[0], r2[0], r3[0], )
+ except sqlite3.DatabaseError as err:
+ self._handle_database_corruption( err )
+ raise DatabaseCorrupted( 'Database error during critical operation: {} - Database will be rebuilt from scratch.'.format( err ) )
+
+ def ftUpdateEnd( self, delete ):
+ try:
+ cursor = self.conn.cursor()
+ cursor.execute( 'SELECT COUNT(*) FROM `channel` WHERE ( touched = 0 )' )
+ ( del_chn, ) = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `show` WHERE ( touched = 0 )' )
+ ( del_shw, ) = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `film` WHERE ( touched = 0 )' )
+ ( del_mov, ) = cursor.fetchone()
+ if delete:
+ cursor.execute( 'DELETE FROM `show` WHERE ( show.touched = 0 ) AND ( ( SELECT SUM( film.touched ) FROM `film` WHERE film.showid = show.id ) = 0 )' )
+ cursor.execute( 'DELETE FROM `film` WHERE ( touched = 0 )' )
+ else:
+ del_chn = 0
+ del_shw = 0
+ del_mov = 0
+ cursor.execute( 'SELECT COUNT(*) FROM `channel`' )
+ ( cnt_chn, ) = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `show`' )
+ ( cnt_shw, ) = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `film`' )
+ ( cnt_mov, ) = cursor.fetchone()
+ cursor.close()
+ self.conn.commit()
+ return ( del_chn, del_shw, del_mov, cnt_chn, cnt_shw, cnt_mov, )
+ except sqlite3.DatabaseError as err:
+ self._handle_database_corruption( err )
+ raise DatabaseCorrupted( 'Database error during critical operation: {} - Database will be rebuilt from scratch.'.format( err ) )
+
+ def ftInsertFilm( self, film, commit ):
+ try:
+ cursor = self.conn.cursor()
+ newchn = False
+ inschn = 0
+ insshw = 0
+ insmov = 0
+
+ # handle channel
+ if self.ft_channel != film['channel']:
+ # process changed channel
+ newchn = True
+ cursor.execute( 'SELECT `id`,`touched` FROM `channel` WHERE channel.channel=?', ( film['channel'], ) )
+ r = cursor.fetchall()
+ if len( r ) > 0:
+ # get the channel data
+ self.ft_channel = film['channel']
+ self.ft_channelid = r[0][0]
+ if r[0][1] == 0:
+ # updated touched
+ cursor.execute( 'UPDATE `channel` SET `touched`=1 WHERE ( channel.id=? )', ( self.ft_channelid, ) )
+ else:
+ # insert the new channel
+ inschn = 1
+ cursor.execute( 'INSERT INTO `channel` ( `dtCreated`,`channel` ) VALUES ( ?,? )', ( int( time.time() ), film['channel'] ) )
+ self.ft_channel = film['channel']
+ self.ft_channelid = cursor.lastrowid
+
+ # handle show
+ if newchn or self.ft_show != film['show']:
+ # process changed show
+ cursor.execute( 'SELECT `id`,`touched` FROM `show` WHERE ( show.channelid=? ) AND ( show.show=? )', ( self.ft_channelid, film['show'] ) )
+ r = cursor.fetchall()
+ if len( r ) > 0:
+ # get the show data
+ self.ft_show = film['show']
+ self.ft_showid = r[0][0]
+ if r[0][1] == 0:
+ # updated touched
+ cursor.execute( 'UPDATE `show` SET `touched`=1 WHERE ( show.id=? )', ( self.ft_showid, ) )
+ else:
+ # insert the new show
+ insshw = 1
+ cursor.execute(
+ """
+ INSERT INTO `show` (
+ `dtCreated`,
+ `channelid`,
+ `show`,
+ `search`
+ )
+ VALUES (
+ ?,
+ ?,
+ ?,
+ ?
+ )
+ """, (
+ int( time.time() ),
+ self.ft_channelid, film['show'],
+ self._make_search( film['show'] )
+ )
+ )
+ self.ft_show = film['show']
+ self.ft_showid = cursor.lastrowid
+
+ # check if the movie is there
+ cursor.execute( """
+ SELECT `id`,
+ `touched`
+ FROM `film`
+ WHERE ( film.channelid = ? )
+ AND
+ ( film.showid = ? )
+ AND
+ ( film.url_video = ? )
+ """, ( self.ft_channelid, self.ft_showid, film['url_video'] ) )
+ r = cursor.fetchall()
+ if len( r ) > 0:
+ # film found
+ filmid = r[0][0]
+ if r[0][1] == 0:
+ # update touched
+ cursor.execute( 'UPDATE `film` SET `touched`=1 WHERE ( film.id=? )', ( filmid, ) )
+ else:
+ # insert the new film
+ insmov = 1
+ cursor.execute(
+ """
+ INSERT INTO `film` (
+ `dtCreated`,
+ `channelid`,
+ `showid`,
+ `title`,
+ `search`,
+ `aired`,
+ `duration`,
+ `size`,
+ `description`,
+ `website`,
+ `url_sub`,
+ `url_video`,
+ `url_video_sd`,
+ `url_video_hd`
+ )
+ VALUES (
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?
+ )
+ """, (
+ int( time.time() ),
+ self.ft_channelid,
+ self.ft_showid,
+ film['title'],
+ self._make_search( film['title'] ),
+ film['airedepoch'],
+ self._make_duration( film['duration'] ),
+ film['size'],
+ film['description'],
+ film['website'],
+ film['url_sub'],
+ film['url_video'],
+ film['url_video_sd'],
+ film['url_video_hd']
+ )
+ )
+ filmid = cursor.lastrowid
+ if commit:
+ self.conn.commit()
+ cursor.close()
+ return ( filmid, inschn, insshw, insmov )
+ except sqlite3.DatabaseError as err:
+ self._handle_database_corruption( err )
+ raise DatabaseCorrupted( 'Database error during critical operation: {} - Database will be rebuilt from scratch.'.format( err ) )
+
+ def _handle_database_corruption( self, err ):
+ self.logger.error( 'Database error during critical operation: {} - Database will be rebuilt from scratch.', err )
+ self.notifier.ShowDatabaseError( err )
+ self.Exit()
+ self.Init( reset = True )
+
+ def _handle_database_initialization( self ):
+ self.conn.executescript( """
+PRAGMA foreign_keys = false;
+
+-- ----------------------------
+-- Table structure for channel
+-- ----------------------------
+DROP TABLE IF EXISTS "channel";
+CREATE TABLE "channel" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "dtCreated" integer(11,0) NOT NULL DEFAULT 0,
+ "touched" integer(1,0) NOT NULL DEFAULT 1,
+ "channel" TEXT(255,0) NOT NULL
+);
+
+-- ----------------------------
+-- Table structure for film
+-- ----------------------------
+DROP TABLE IF EXISTS "film";
+CREATE TABLE "film" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "dtCreated" integer(11,0) NOT NULL DEFAULT 0,
+ "touched" integer(1,0) NOT NULL DEFAULT 1,
+ "channelid" INTEGER(11,0) NOT NULL,
+ "showid" INTEGER(11,0) NOT NULL,
+ "title" TEXT(255,0) NOT NULL,
+ "search" TEXT(255,0) NOT NULL,
+ "aired" integer(11,0),
+ "duration" integer(11,0),
+ "size" integer(11,0),
+ "description" TEXT(2048,0),
+ "website" TEXT(384,0),
+ "url_sub" TEXT(384,0),
+ "url_video" TEXT(384,0),
+ "url_video_sd" TEXT(384,0),
+ "url_video_hd" TEXT(384,0),
+ CONSTRAINT "FK_FilmShow" FOREIGN KEY ("showid") REFERENCES "show" ("id") ON DELETE CASCADE,
+ CONSTRAINT "FK_FilmChannel" FOREIGN KEY ("channelid") REFERENCES "channel" ("id") ON DELETE CASCADE
+);
+
+-- ----------------------------
+-- Table structure for show
+-- ----------------------------
+DROP TABLE IF EXISTS "show";
+CREATE TABLE "show" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "dtCreated" integer(11,0) NOT NULL DEFAULT 0,
+ "touched" integer(1,0) NOT NULL DEFAULT 1,
+ "channelid" INTEGER(11,0) NOT NULL DEFAULT 0,
+ "show" TEXT(255,0) NOT NULL,
+ "search" TEXT(255,0) NOT NULL,
+ CONSTRAINT "FK_ShowChannel" FOREIGN KEY ("channelid") REFERENCES "channel" ("id") ON DELETE CASCADE
+);
+
+-- ----------------------------
+-- Table structure for status
+-- ----------------------------
+DROP TABLE IF EXISTS "status";
+CREATE TABLE "status" (
+ "modified" integer(11,0),
+ "status" TEXT(32,0),
+ "lastupdate" integer(11,0),
+ "filmupdate" integer(11,0),
+ "fullupdate" integer(1,0),
+ "add_chn" integer(11,0),
+ "add_shw" integer(11,0),
+ "add_mov" integer(11,0),
+ "del_chm" integer(11,0),
+ "del_shw" integer(11,0),
+ "del_mov" integer(11,0),
+ "tot_chn" integer(11,0),
+ "tot_shw" integer(11,0),
+ "tot_mov" integer(11,0)
+);
+
+-- ----------------------------
+-- Indexes structure for table film
+-- ----------------------------
+CREATE INDEX "dupecheck" ON film ("channelid", "showid", "url_video");
+CREATE INDEX "index_1" ON film ("channelid", "title" COLLATE NOCASE);
+CREATE INDEX "index_2" ON film ("showid", "title" COLLATE NOCASE);
+
+-- ----------------------------
+-- Indexes structure for table show
+-- ----------------------------
+CREATE INDEX "category" ON show ("category");
+CREATE INDEX "search" ON show ("search");
+CREATE INDEX "combined_1" ON show ("channelid", "search");
+CREATE INDEX "combined_2" ON show ("channelid", "show");
+
+PRAGMA foreign_keys = true;
+ """ )
+ self.UpdateStatus( 'IDLE' )
+
+ def _make_search( self, val ):
+ cset = string.letters + string.digits + ' _-#'
+ search = ''.join( [ c for c in val if c in cset ] )
+ return search.upper().strip()
+
+ def _make_duration( self, val ):
+ if val == "00:00:00":
+ return None
+ elif val is None:
+ return None
+ x = val.split( ':' )
+ if len( x ) != 3:
+ return None
+ return int( x[0] ) * 3600 + int( x[1] ) * 60 + int( x[2] )
+
+ def _dir_exists( self, name ):
+ try:
+ s = os.stat( name )
+ return stat.S_ISDIR( s.st_mode )
+ except OSError as err:
+ return False
+
+ def _file_exists( self, name ):
+ try:
+ s = os.stat( name )
+ return stat.S_ISREG( s.st_mode )
+ except OSError as err:
+ return False
+
+ def _file_remove( self, name ):
+ if self._file_exists( name ):
+ try:
+ os.remove( name )
+ return True
+ except OSError as err:
+ self.logger.error( 'Failed to remove {}: error {}', name, err )
+ return False
+
+def UNIX_TIMESTAMP():
+ return int( time.time() )
+
+class GROUP_CONCAT:
+ def __init__( self ):
+ self.value = ''
+
+ def step( self, value ):
+ if value is not None:
+ if self.value == '':
+ self.value = '{0}'.format( value )
+ else:
+ self.value = '{0},{1}'.format( self.value, value )
+
+ def finalize(self):
+ return self.value
diff --git a/plugin.video.mediathekview/classes/ttml2srt.py b/plugin.video.mediathekview/classes/ttml2srt.py
new file mode 100644
index 0000000..e36b41e
--- /dev/null
+++ b/plugin.video.mediathekview/classes/ttml2srt.py
@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Laura Klünder
+# See https://github.com/codingcatgirl/ttml2srt
+#
+# MIT License
+#
+# Copyright (c) 2017 Laura Klünder
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import re
+import io
+import sys
+from datetime import timedelta
+from xml.etree import ElementTree as ET
+
+def ttml2srt( infile, outfile ):
+ tree = ET.parse( infile )
+ root = tree.getroot()
+
+ # strip namespaces
+ for elem in root.getiterator():
+ elem.tag = elem.tag.split('}', 1)[-1]
+ elem.attrib = {name.split('}', 1)
+ [-1]: value for name, value in elem.attrib.items()}
+
+ # get styles
+ styles = {}
+ for elem in root.findall('./head/styling/style'):
+ style = {}
+ if 'color' in elem.attrib:
+ color = elem.attrib['color']
+ if color not in ('#FFFFFF', '#000000'):
+ style['color'] = color
+ if 'fontStyle' in elem.attrib:
+ fontstyle = elem.attrib['fontStyle']
+ if fontstyle in ('italic', ):
+ style['fontstyle'] = fontstyle
+ styles[elem.attrib['id']] = style
+
+ body = root.find('./body')
+
+ # parse correct start and end times
+ def parse_time_expression(expression, default_offset=timedelta(0)):
+ offset_time = re.match(r'^([0-9]+(\.[0-9]+)?)(h|m|s|ms|f|t)$', expression)
+ if offset_time:
+ time_value, fraction, metric = offset_time.groups()
+ time_value = float(time_value)
+ if metric == 'h':
+ return default_offset + timedelta(hours=time_value)
+ elif metric == 'm':
+ return default_offset + timedelta(minutes=time_value)
+ elif metric == 's':
+ return default_offset + timedelta(seconds=time_value)
+ elif metric == 'ms':
+ return default_offset + timedelta(milliseconds=time_value)
+ elif metric == 'f':
+ raise NotImplementedError(
+ 'Parsing time expressions by frame is not supported!')
+ elif metric == 't':
+ raise NotImplementedError(
+ 'Parsing time expressions by ticks is not supported!')
+
+ clock_time = re.match(
+ r'^([0-9]{2,}):([0-9]{2,}):([0-9]{2,}(\.[0-9]+)?)$', expression)
+ if clock_time:
+ hours, minutes, seconds, fraction = clock_time.groups()
+ return timedelta(hours=int(hours), minutes=int(minutes), seconds=float(seconds))
+
+ clock_time_frames = re.match(
+ r'^([0-9]{2,}):([0-9]{2,}):([0-9]{2,}):([0-9]{2,}(\.[0-9]+)?)$', expression)
+ if clock_time_frames:
+ raise NotImplementedError(
+ 'Parsing time expressions by frame is not supported!')
+
+ raise ValueError('unknown time expression: %s' % expression)
+
+
+ def parse_times(elem, default_begin=timedelta(0)):
+ if 'begin' in elem.attrib:
+ begin = parse_time_expression(
+ elem.attrib['begin'], default_offset=default_begin)
+ else:
+ begin = default_begin
+ elem.attrib['{abs}begin'] = begin
+
+ end = None
+ if 'end' in elem.attrib:
+ end = parse_time_expression(
+ elem.attrib['end'], default_offset=default_begin)
+
+ dur = None
+ if 'dur' in elem.attrib:
+ dur = parse_time_expression(elem.attrib['dur'])
+
+ if dur is not None:
+ if end is None:
+ end = begin + dur
+ else:
+ end = min(end, begin + dur)
+
+ elem.attrib['{abs}end'] = end
+
+ for child in elem:
+ parse_times(child, default_begin=begin)
+
+
+ parse_times(body)
+
+ timestamps = set()
+ for elem in body.findall('.//*[@{abs}begin]'):
+ timestamps.add(elem.attrib['{abs}begin'])
+
+ for elem in body.findall('.//*[@{abs}end]'):
+ timestamps.add(elem.attrib['{abs}end'])
+
+ timestamps.discard(None)
+
+ # render subtitles on each timestamp
+
+
+ def render_subtitles(elem, timestamp, parent_style={}):
+
+ if timestamp < elem.attrib['{abs}begin']:
+ return ''
+ if elem.attrib['{abs}end'] is not None and timestamp >= elem.attrib['{abs}end']:
+ return ''
+
+ result = ''
+
+ style = parent_style.copy()
+ if 'style' in elem.attrib:
+ style.update(styles[elem.attrib['style']])
+
+ if 'color' in style:
+ result += '<font color="%s">' % style['color']
+
+ if style.get('fontstyle') == 'italic':
+ result += '<i>'
+
+ if elem.text:
+ result += elem.text.strip()
+ if len(elem):
+ for child in elem:
+ result += render_subtitles(child, timestamp)
+ if child.tail:
+ result += child.tail.strip()
+
+ if 'color' in style:
+ result += '</font>'
+
+ if style.get('fontstyle') == 'italic':
+ result += '</i>'
+
+ if elem.tag in ('div', 'p', 'br'):
+ result += '\n'
+
+ return result
+
+
+ rendered = []
+ for timestamp in sorted(timestamps):
+ rendered.append((timestamp, re.sub(r'\n\n\n+', '\n\n',
+ render_subtitles(body, timestamp)).strip()))
+
+ if not rendered:
+ exit(0)
+
+ # group timestamps together if nothing changes
+ rendered_grouped = []
+ last_text = None
+ for timestamp, content in rendered:
+ if content != last_text:
+ rendered_grouped.append((timestamp, content))
+ last_text = content
+
+ # output srt
+ rendered_grouped.append((rendered_grouped[-1][0] + timedelta(hours=24), ''))
+
+
+ def format_timestamp(timestamp):
+ return ('%02d:%02d:%02.3f' % (timestamp.total_seconds() // 3600,
+ timestamp.total_seconds() // 60 % 60,
+ timestamp.total_seconds() % 60)).replace('.', ',')
+
+
+ if type( outfile ) is str or type( outfile ) is unicode:
+ file = io.open( outfile, 'w', encoding='utf-8' )
+ else:
+ file = outfile
+
+ srt_i = 1
+ for i, (timestamp, content) in enumerate(rendered_grouped[:-1]):
+ if content == '':
+ continue
+ file.write( bytearray( '%d\n' % srt_i, 'utf-8' ) )
+ file.write( bytearray(
+ format_timestamp( timestamp ) +
+ ' --> ' +
+ format_timestamp( rendered_grouped[i + 1][0] ) +
+ '\n'
+ ) )
+ file.write( bytearray( content + '\n\n', 'utf-8' ) )
+ srt_i += 1
+ file.close()
diff --git a/plugin.video.mediathekview/classes/updater.py b/plugin.video.mediathekview/classes/updater.py
new file mode 100644
index 0000000..8609303
--- /dev/null
+++ b/plugin.video.mediathekview/classes/updater.py
@@ -0,0 +1,431 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2018, Leo Moll
+
+# -- Imports ------------------------------------------------
+import os, stat, urllib, urllib2, subprocess, ijson, datetime, time
+import xml.etree.ElementTree as etree
+
+from operator import itemgetter
+from classes.store import Store
+from classes.exceptions import DatabaseCorrupted
+from classes.exceptions import DatabaseLost
+
+# -- Constants ----------------------------------------------
+FILMLISTE_AKT_URL = 'https://res.mediathekview.de/akt.xml'
+FILMLISTE_DIF_URL = 'https://res.mediathekview.de/diff.xml'
+
+# -- Classes ------------------------------------------------
+class MediathekViewUpdater( object ):
+ def __init__( self, logger, notifier, settings, monitor = None ):
+ self.logger = logger
+ self.notifier = notifier
+ self.settings = settings
+ self.monitor = monitor
+ self.db = None
+
+ def Init( self ):
+ if self.db is not None:
+ self.Exit()
+ self.db = Store( self.logger, self.notifier, self.settings )
+ self.db.Init()
+
+ def Exit( self ):
+ if self.db is not None:
+ self.db.Exit()
+ del self.db
+ self.db = None
+
+ def PrerequisitesMissing( self ):
+ return self.settings.updenabled and self._find_xz() is None
+
+ def IsEnabled( self ):
+ if self.settings.updenabled:
+ xz = self._find_xz()
+ return xz is not None
+
+ def GetCurrentUpdateOperation( self ):
+ if not self.IsEnabled() or self.db is None:
+ # update disabled or not possible
+ self.logger.info( 'update disabled or not possible' )
+ return 0
+ status = self.db.GetStatus()
+ tsnow = int( time.time() )
+ tsold = status['lastupdate']
+ dtnow = datetime.datetime.fromtimestamp( tsnow ).date()
+ dtold = datetime.datetime.fromtimestamp( tsold ).date()
+ if status['status'] == 'UNINIT':
+ # database not initialized
+ self.logger.debug( 'database not initialized' )
+ return 0
+ elif status['status'] == "UPDATING" and tsnow - tsold > 86400:
+ # process was probably killed during update
+ self.logger.info( 'Stuck update pretending to run since epoch {} reset', tsold )
+ self.db.UpdateStatus( 'ABORTED' )
+ return 0
+ elif status['status'] == "UPDATING":
+ # already updating
+ self.logger.debug( 'already updating' )
+ return 0
+ elif tsnow - tsold < self.settings.updinterval:
+ # last update less than the configured update interval. do nothing
+ self.logger.debug( 'last update less than the configured update interval. do nothing' )
+ return 0
+ elif dtnow != dtold:
+ # last update was not today. do full update once a day
+ self.logger.debug( 'last update was not today. do full update once a day' )
+ return 1
+ elif status['status'] == "ABORTED" and status['fullupdate'] == 1:
+ # last full update was aborted - full update needed
+ self.logger.debug( 'last full update was aborted - full update needed' )
+ return 1
+ else:
+ # do differential update
+ self.logger.debug( 'do differential update' )
+ return 2
+
+ def Update( self, full ):
+ if self.db is None:
+ return
+ if self.db.SupportsUpdate():
+ if self.GetNewestList( full ):
+ self.Import( full )
+
+ def Import( self, full ):
+ ( url, compfile, destfile, avgrecsize ) = self._get_update_info( full )
+ if not self._file_exists( destfile ):
+ self.logger.error( 'File {} does not exists', destfile )
+ return False
+ # estimate number of records in update file
+ records = int( self._file_size( destfile ) / avgrecsize )
+ if not self.db.ftInit():
+ self.logger.warn( 'Failed to initialize update. Maybe a concurrency problem?' )
+ return False
+ try:
+ self.logger.info( 'Starting import of approx. {} records from {}', records, destfile )
+ file = open( destfile, 'r' )
+ parser = ijson.parse( file )
+ flsm = 0
+ flts = 0
+ ( self.tot_chn, self.tot_shw, self.tot_mov ) = self._update_start( full )
+ self.notifier.ShowUpdateProgress()
+ for prefix, event, value in parser:
+ if ( prefix, event ) == ( "X", "start_array" ):
+ self._init_record()
+ elif ( prefix, event ) == ( "X", "end_array" ):
+ self._end_record( records )
+ if self.count % 100 == 0 and self.monitor.abortRequested():
+ # kodi is shutting down. Close all
+ file.close()
+ self._update_end( full, 'ABORTED' )
+ self.notifier.CloseUpdateProgress()
+ return True
+ elif ( prefix, event ) == ( "X.item", "string" ):
+ if value is not None:
+# self._add_value( value.strip().encode('utf-8') )
+ self._add_value( value.strip() )
+ else:
+ self._add_value( "" )
+ elif ( prefix, event ) == ( "Filmliste", "start_array" ):
+ flsm += 1
+ elif ( prefix, event ) == ( "Filmliste.item", "string" ):
+ flsm += 1
+ if flsm == 2 and value is not None:
+ # this is the timestmap of this database update
+ try:
+ fldt = datetime.datetime.strptime( value.strip(), "%d.%m.%Y, %H:%M" )
+ flts = int( time.mktime( fldt.timetuple() ) )
+ self.db.UpdateStatus( filmupdate = flts )
+ self.logger.info( 'Filmliste dated {}', value.strip() )
+ except TypeError:
+ # SEE: https://forum.kodi.tv/showthread.php?tid=112916&pid=1214507#pid1214507
+ # Wonderful. His name is also Leopold
+ try:
+ flts = int( time.mktime( time.strptime( value.strip(), "%d.%m.%Y, %H:%M" ) ) )
+ self.db.UpdateStatus( filmupdate = flts )
+ self.logger.info( 'Filmliste dated {}', value.strip() )
+ except Exception as err:
+ # If the universe hates us...
+ pass
+ except ValueError as err:
+ pass
+
+ file.close()
+ self._update_end( full, 'IDLE' )
+ self.logger.info( 'Import of {} finished', destfile )
+ self.notifier.CloseUpdateProgress()
+ return True
+ except KeyboardInterrupt:
+ file.close()
+ self._update_end( full, 'ABORTED' )
+ self.logger.info( 'Interrupted by user' )
+ self.notifier.CloseUpdateProgress()
+ return True
+ except DatabaseCorrupted as err:
+ self.logger.error( '{}', err )
+ self.notifier.CloseUpdateProgress()
+ file.close()
+ except DatabaseLost as err:
+ self.logger.error( '{}', err )
+ self.notifier.CloseUpdateProgress()
+ file.close()
+ except IOError as err:
+ self.logger.error( 'Error {} wile processing {}', err, destfile )
+ try:
+ self._update_end( full, 'ABORTED' )
+ self.notifier.CloseUpdateProgress()
+ file.close()
+ except Exception as err:
+ pass
+ return False
+
+ def GetNewestList( self, full ):
+ # get xz binary
+ xzbin = self._find_xz()
+ if xzbin is None:
+ self.notifier.ShowMissingXZError()
+ return False
+
+ ( url, compfile, destfile, avgrecsize ) = self._get_update_info( full )
+
+ # get mirrorlist
+ self.logger.info( 'Opening {}', url )
+ try:
+ data = urllib2.urlopen( url ).read()
+ except urllib2.URLError as err:
+ self.logger.error( 'Failure opening {}', url )
+ self.notifier.ShowDowloadError( url, err )
+ return False
+
+ root = etree.fromstring ( data )
+ urls = []
+ for server in root.findall( 'Server' ):
+ try:
+ URL = server.find( 'URL' ).text
+ Prio = server.find( 'Prio' ).text
+ urls.append( ( URL, Prio ) )
+ self.logger.info( 'Found mirror {} (Priority {})', URL, Prio )
+ except AttributeError as error:
+ pass
+ urls = sorted( urls, key = itemgetter( 1 ) )
+ urls = [ url[0] for url in urls ]
+ result = None
+
+ # cleanup downloads
+ self.logger.info( 'Cleaning up old downloads...' )
+ self._file_remove( compfile )
+ self._file_remove( destfile )
+
+ # download filmliste
+ self.logger.info( 'Trying to download file...' )
+ self.notifier.ShowDownloadProgress()
+ lasturl = ''
+ for url in urls:
+ try:
+ lasturl = url
+ self.notifier.UpdateDownloadProgress( 0, url )
+ result = urllib.urlretrieve( url, filename = compfile, reporthook = self._reporthook )
+ break
+ except IOError as err:
+ self.logger.error( 'Failure opening {}', url )
+ if result is None:
+ self.logger.info( 'No file downloaded' )
+ self.notifier.CloseDownloadProgress()
+ self.notifier.ShowDowloadError( lasturl, err )
+ return False
+
+ # decompress filmliste
+ self.logger.info( 'Trying to decompress file...' )
+ retval = subprocess.call( [ xzbin, '-d', compfile ] )
+ self.logger.info( 'Return {}', retval )
+ self.notifier.CloseDownloadProgress()
+ return retval == 0 and self._file_exists( destfile )
+
+ def _get_update_info( self, full ):
+ if full:
+ return (
+ FILMLISTE_AKT_URL,
+ os.path.join( self.settings.datapath, 'Filmliste-akt.xz' ),
+ os.path.join( self.settings.datapath, 'Filmliste-akt' ),
+ 600,
+ )
+ else:
+ return (
+ FILMLISTE_DIF_URL,
+ os.path.join( self.settings.datapath, 'Filmliste-diff.xz' ),
+ os.path.join( self.settings.datapath, 'Filmliste-diff' ),
+ 700,
+ )
+
+ def _find_xz( self ):
+ for xzbin in [ '/bin/xz', '/usr/bin/xz', '/usr/local/bin/xz' ]:
+ if self._file_exists( xzbin ):
+ return xzbin
+ if self.settings.updxzbin != '' and self._file_exists( self.settings.updxzbin ):
+ return self.settings.updxzbin
+ return None
+
+ def _file_exists( self, name ):
+ try:
+ s = os.stat( name )
+ return stat.S_ISREG( s.st_mode )
+ except OSError as err:
+ return False
+
+ def _file_size( self, name ):
+ try:
+ s = os.stat( name )
+ return s.st_size
+ except OSError as err:
+ return 0
+
+ def _file_remove( self, name ):
+ if self._file_exists( name ):
+ try:
+ os.remove( name )
+ return True
+ except OSError as err:
+ self.logger.error( 'Failed to remove {}: error {}', name, err )
+ return False
+
+ def _reporthook( self, blockcount, blocksize, totalsize ):
+ downloaded = blockcount * blocksize
+ if totalsize > 0:
+ percent = int( (downloaded * 100) / totalsize )
+ self.notifier.UpdateDownloadProgress( percent )
+ self.logger.debug( 'Downloading blockcount={}, blocksize={}, totalsize={}', blockcount, blocksize, totalsize )
+
+ def _update_start( self, full ):
+ self.logger.info( 'Initializing update...' )
+ self.add_chn = 0
+ self.add_shw = 0
+ self.add_mov = 0
+ self.add_chn = 0
+ self.add_shw = 0
+ self.add_mov = 0
+ self.del_chn = 0
+ self.del_shw = 0
+ self.del_mov = 0
+ self.index = 0
+ self.count = 0
+ self.film = {
+ "channel": "",
+ "show": "",
+ "title": "",
+ "aired": "1980-01-01 00:00:00",
+ "duration": "00:00:00",
+ "size": 0,
+ "description": "",
+ "website": "",
+ "url_sub": "",
+ "url_video": "",
+ "url_video_sd": "",
+ "url_video_hd": "",
+ "airedepoch": 0,
+ "geo": ""
+ }
+ return self.db.ftUpdateStart( full )
+
+ def _update_end( self, full, status ):
+ self.logger.info( 'Added: channels:%d, shows:%d, movies:%d ...' % ( self.add_chn, self.add_shw, self.add_mov ) )
+ ( self.del_chn, self.del_shw, self.del_mov, self.tot_chn, self.tot_shw, self.tot_mov ) = self.db.ftUpdateEnd( full and status == 'IDLE' )
+ self.logger.info( 'Deleted: channels:%d, shows:%d, movies:%d' % ( self.del_chn, self.del_shw, self.del_mov ) )
+ self.logger.info( 'Total: channels:%d, shows:%d, movies:%d' % ( self.tot_chn, self.tot_shw, self.tot_mov ) )
+ self.db.UpdateStatus(
+ status,
+ int( time.time() ) if status != 'ABORTED' else None,
+ None,
+ 1 if full else 0,
+ self.add_chn, self.add_shw, self.add_mov,
+ self.del_chn, self.del_shw, self.del_mov,
+ self.tot_chn, self.tot_shw, self.tot_mov
+ )
+
+ def _init_record( self ):
+ self.index = 0
+ self.film["title"] = ""
+ self.film["aired"] = "1980-01-01 00:00:00"
+ self.film["duration"] = "00:00:00"
+ self.film["size"] = 0
+ self.film["description"] = ""
+ self.film["website"] = ""
+ self.film["url_sub"] = ""
+ self.film["url_video"] = ""
+ self.film["url_video_sd"] = ""
+ self.film["url_video_hd"] = ""
+ self.film["airedepoch"] = 0
+ self.film["geo"] = ""
+
+ def _end_record( self, records ):
+ if self.count % 1000 == 0:
+ percent = int( self.count * 100 / records )
+ self.logger.info( 'In progress (%d%%): channels:%d, shows:%d, movies:%d ...' % ( percent, self.add_chn, self.add_shw, self.add_mov ) )
+ self.notifier.UpdateUpdateProgress( percent if percent <= 100 else 100, self.count, self.add_chn, self.add_shw, self.add_mov )
+ self.db.UpdateStatus(
+ add_chn = self.add_chn,
+ add_shw = self.add_shw,
+ add_mov = self.add_mov,
+ tot_chn = self.tot_chn + self.add_chn,
+ tot_shw = self.tot_shw + self.add_shw,
+ tot_mov = self.tot_mov + self.add_mov
+ )
+ self.count = self.count + 1
+ ( filmid, cnt_chn, cnt_shw, cnt_mov ) = self.db.ftInsertFilm( self.film, True )
+ else:
+ self.count = self.count + 1
+ ( filmid, cnt_chn, cnt_shw, cnt_mov ) = self.db.ftInsertFilm( self.film, False )
+ self.add_chn += cnt_chn
+ self.add_shw += cnt_shw
+ self.add_mov += cnt_mov
+
+ def _add_value( self, val ):
+ if self.index == 0:
+ if val != "":
+ self.film["channel"] = val
+ elif self.index == 1:
+ if val != "":
+ self.film["show"] = val[:255]
+ elif self.index == 2:
+ self.film["title"] = val[:255]
+ elif self.index == 3:
+ if len(val) == 10:
+ self.film["aired"] = val[6:] + '-' + val[3:5] + '-' + val[:2]
+ elif self.index == 4:
+ if ( self.film["aired"] != "1980-01-01 00:00:00" ) and ( len(val) == 8 ):
+ self.film["aired"] = self.film["aired"] + " " + val
+ elif self.index == 5:
+ if len(val) == 8:
+ self.film["duration"] = val
+ elif self.index == 6:
+ if val != "":
+ self.film["size"] = int(val)
+ elif self.index == 7:
+ self.film["description"] = val
+ elif self.index == 8:
+ self.film["url_video"] = val
+ elif self.index == 9:
+ self.film["website"] = val
+ elif self.index == 10:
+ self.film["url_sub"] = val
+ elif self.index == 12:
+ self.film["url_video_sd"] = self._make_url(val)
+ elif self.index == 14:
+ self.film["url_video_hd"] = self._make_url(val)
+ elif self.index == 16:
+ if val != "":
+ self.film["airedepoch"] = int(val)
+ elif self.index == 18:
+ self.film["geo"] = val
+ self.index = self.index + 1
+
+ def _make_search( self, val ):
+ cset = string.letters + string.digits + ' _-#'
+ search = ''.join( [ c for c in val if c in cset ] )
+ return search.upper().strip()
+
+ def _make_url( self, val ):
+ x = val.split( '|' )
+ if len( x ) == 2:
+ cnt = int( x[0] )
+ return self.film["url_video"][:cnt] + x[1]
+ else:
+ return val