From 144c832baedd948102c2ee0ef0c9ac5b118f8b6e Mon Sep 17 00:00:00 2001 From: Leo Moll Date: Sun, 14 Jan 2018 22:25:12 +0100 Subject: [plugin.video.mediathekview] 0.4.0 --- plugin.video.mediathekview/README.md | 164 ++-- plugin.video.mediathekview/addon.py | 114 +-- plugin.video.mediathekview/addon.xml | 25 +- plugin.video.mediathekview/classes/__init__.py | 0 plugin.video.mediathekview/classes/channel.py | 11 - plugin.video.mediathekview/classes/channelui.py | 42 - plugin.video.mediathekview/classes/exceptions.py | 9 - plugin.video.mediathekview/classes/film.py | 21 - plugin.video.mediathekview/classes/filmui.py | 105 --- plugin.video.mediathekview/classes/initialui.py | 47 -- plugin.video.mediathekview/classes/mvupdate.py | 200 ----- plugin.video.mediathekview/classes/notifier.py | 42 - plugin.video.mediathekview/classes/settings.py | 37 - plugin.video.mediathekview/classes/show.py | 13 - plugin.video.mediathekview/classes/showui.py | 47 -- plugin.video.mediathekview/classes/store.py | 127 --- plugin.video.mediathekview/classes/storemysql.py | 534 ------------ plugin.video.mediathekview/classes/storesqlite.py | 766 ----------------- plugin.video.mediathekview/classes/ttml2srt.py | 221 ----- plugin.video.mediathekview/classes/updater.py | 431 ---------- plugin.video.mediathekview/de/__init__.py | 0 plugin.video.mediathekview/de/yeasoft/__init__.py | 0 .../de/yeasoft/base/Logger.py | 33 - .../de/yeasoft/base/__init__.py | 0 .../de/yeasoft/kodi/KodiAddon.py | 68 -- .../de/yeasoft/kodi/KodiLogger.py | 55 -- .../de/yeasoft/kodi/KodiUI.py | 76 -- .../de/yeasoft/kodi/__init__.py | 0 plugin.video.mediathekview/resources/__init__.py | 0 plugin.video.mediathekview/resources/fanart.jpg | Bin 148223 -> 178572 bytes plugin.video.mediathekview/resources/icon.jpg | Bin 195418 -> 0 bytes plugin.video.mediathekview/resources/icon.png | Bin 0 -> 484822 bytes .../resources/language/English/strings.po | 300 ------- .../resources/language/German/strings.po | 302 ------- .../resources/language/Italian/strings.po | 303 ------- .../language/resource.language.de_de/strings.po | 310 +++++++ .../language/resource.language.en_gb/strings.po | 308 +++++++ .../language/resource.language.it_it/strings.po | 311 +++++++ .../resources/lib/__init__.py | 0 .../resources/lib/base/Logger.py | 33 + .../resources/lib/base/__init__.py | 0 .../resources/lib/channel.py | 11 + .../resources/lib/channelui.py | 41 + .../resources/lib/exceptions.py | 9 + plugin.video.mediathekview/resources/lib/film.py | 21 + plugin.video.mediathekview/resources/lib/filmui.py | 115 +++ .../resources/lib/initialui.py | 47 ++ .../resources/lib/kodi/KodiAddon.py | 68 ++ .../resources/lib/kodi/KodiLogger.py | 38 + .../resources/lib/kodi/KodiUI.py | 76 ++ .../resources/lib/kodi/__init__.py | 0 .../resources/lib/mvupdate.py | 201 +++++ .../resources/lib/mvutils.py | 87 ++ .../resources/lib/notifier.py | 45 + .../resources/lib/settings.py | 37 + plugin.video.mediathekview/resources/lib/show.py | 13 + plugin.video.mediathekview/resources/lib/showui.py | 46 ++ plugin.video.mediathekview/resources/lib/store.py | 122 +++ .../resources/lib/storemysql.py | 913 +++++++++++++++++++++ .../resources/lib/storesqlite.py | 753 +++++++++++++++++ .../resources/lib/ttml2srt.py | 220 +++++ .../resources/lib/updater.py | 466 +++++++++++ plugin.video.mediathekview/resources/settings.xml | 20 +- .../resources/sql/exportstruct.sh | 3 - .../resources/sql/filmliste-mysql-v1.sql | 454 ---------- .../resources/sql/filmliste-sqlite-v1.sql | 104 --- plugin.video.mediathekview/service.py | 26 +- 67 files changed, 4445 insertions(+), 4546 deletions(-) delete mode 100644 plugin.video.mediathekview/classes/__init__.py delete mode 100644 plugin.video.mediathekview/classes/channel.py delete mode 100644 plugin.video.mediathekview/classes/channelui.py delete mode 100644 plugin.video.mediathekview/classes/exceptions.py delete mode 100644 plugin.video.mediathekview/classes/film.py delete mode 100644 plugin.video.mediathekview/classes/filmui.py delete mode 100644 plugin.video.mediathekview/classes/initialui.py delete mode 100644 plugin.video.mediathekview/classes/mvupdate.py delete mode 100644 plugin.video.mediathekview/classes/notifier.py delete mode 100644 plugin.video.mediathekview/classes/settings.py delete mode 100644 plugin.video.mediathekview/classes/show.py delete mode 100644 plugin.video.mediathekview/classes/showui.py delete mode 100644 plugin.video.mediathekview/classes/store.py delete mode 100644 plugin.video.mediathekview/classes/storemysql.py delete mode 100644 plugin.video.mediathekview/classes/storesqlite.py delete mode 100644 plugin.video.mediathekview/classes/ttml2srt.py delete mode 100644 plugin.video.mediathekview/classes/updater.py delete mode 100644 plugin.video.mediathekview/de/__init__.py delete mode 100644 plugin.video.mediathekview/de/yeasoft/__init__.py delete mode 100644 plugin.video.mediathekview/de/yeasoft/base/Logger.py delete mode 100644 plugin.video.mediathekview/de/yeasoft/base/__init__.py delete mode 100644 plugin.video.mediathekview/de/yeasoft/kodi/KodiAddon.py delete mode 100644 plugin.video.mediathekview/de/yeasoft/kodi/KodiLogger.py delete mode 100644 plugin.video.mediathekview/de/yeasoft/kodi/KodiUI.py delete mode 100644 plugin.video.mediathekview/de/yeasoft/kodi/__init__.py create mode 100644 plugin.video.mediathekview/resources/__init__.py delete mode 100644 plugin.video.mediathekview/resources/icon.jpg create mode 100644 plugin.video.mediathekview/resources/icon.png delete mode 100644 plugin.video.mediathekview/resources/language/English/strings.po delete mode 100644 plugin.video.mediathekview/resources/language/German/strings.po delete mode 100644 plugin.video.mediathekview/resources/language/Italian/strings.po create mode 100644 plugin.video.mediathekview/resources/language/resource.language.de_de/strings.po create mode 100644 plugin.video.mediathekview/resources/language/resource.language.en_gb/strings.po create mode 100644 plugin.video.mediathekview/resources/language/resource.language.it_it/strings.po create mode 100644 plugin.video.mediathekview/resources/lib/__init__.py create mode 100644 plugin.video.mediathekview/resources/lib/base/Logger.py create mode 100644 plugin.video.mediathekview/resources/lib/base/__init__.py create mode 100644 plugin.video.mediathekview/resources/lib/channel.py create mode 100644 plugin.video.mediathekview/resources/lib/channelui.py create mode 100644 plugin.video.mediathekview/resources/lib/exceptions.py create mode 100644 plugin.video.mediathekview/resources/lib/film.py create mode 100644 plugin.video.mediathekview/resources/lib/filmui.py create mode 100644 plugin.video.mediathekview/resources/lib/initialui.py create mode 100644 plugin.video.mediathekview/resources/lib/kodi/KodiAddon.py create mode 100644 plugin.video.mediathekview/resources/lib/kodi/KodiLogger.py create mode 100644 plugin.video.mediathekview/resources/lib/kodi/KodiUI.py create mode 100644 plugin.video.mediathekview/resources/lib/kodi/__init__.py create mode 100644 plugin.video.mediathekview/resources/lib/mvupdate.py create mode 100644 plugin.video.mediathekview/resources/lib/mvutils.py create mode 100644 plugin.video.mediathekview/resources/lib/notifier.py create mode 100644 plugin.video.mediathekview/resources/lib/settings.py create mode 100644 plugin.video.mediathekview/resources/lib/show.py create mode 100644 plugin.video.mediathekview/resources/lib/showui.py create mode 100644 plugin.video.mediathekview/resources/lib/store.py create mode 100644 plugin.video.mediathekview/resources/lib/storemysql.py create mode 100644 plugin.video.mediathekview/resources/lib/storesqlite.py create mode 100644 plugin.video.mediathekview/resources/lib/ttml2srt.py create mode 100644 plugin.video.mediathekview/resources/lib/updater.py delete mode 100755 plugin.video.mediathekview/resources/sql/exportstruct.sh delete mode 100644 plugin.video.mediathekview/resources/sql/filmliste-mysql-v1.sql delete mode 100644 plugin.video.mediathekview/resources/sql/filmliste-sqlite-v1.sql diff --git a/plugin.video.mediathekview/README.md b/plugin.video.mediathekview/README.md index b356e84..485d456 100644 --- a/plugin.video.mediathekview/README.md +++ b/plugin.video.mediathekview/README.md @@ -64,25 +64,16 @@ dies bedeutet, dass eine lokale SQLite-Datenbank benutzt wird, die auch durch das Kodi-System lokal aktualisiert wird. Dies dürfte auch das üblichste Szenario sein. -Dieses Szenario birgt zwei Voraussetzungen die erfüllt sein sollten: -* ein einigermaßen performantes Dateisystem für die Datenbank. Ein Raspberry - mit seiner langsamen SD-Karte ist in diesem Fall sicherlich nicht die - allerbeste Wahl. Das vollständige Update der Datenbank dauert hier um die - 15-20 Minuten. Da dies aber im Hintergrund passiert, kann man unter Umständen - gut damit leben. -* der Entpacker 'xz' auf dem Kodi-System. Um den Datenbank-Aktualisierer zu - benutzen, muss dieses Programm auf dem System in einem der Standard- - Verzeichnisse (/bin, /usr/bin, /usr/local/bin) installiert werden. - Unter Windows bzw. falls das Programm in einem anderen Verzeichnis - installiert ist, muss der Pfad zum Programm in den Addon-Einstellungen - angegeben werden. Sollte der Entpacker nicht vorhanden sein, so gibt - das Addon eine Meldung aus und deaktiviert den Aktualisierungsprozess. - -Das Addon wurde auf verschiedenen Plattformen unter Linux, MacOS und LibreELEC -bzw. OpenELEC getestet. Dort war auch der entsprechende Entpacker verfügbar. -Unter Windows muss der Entpacker nachträglich installiert werden und dessen -Pfad in den Addon-Einstellungen angegeben werden. Mangels Testsystem konnte -dies jedoch zum jetzigen Zeitpunkt noch nicht getestet werden. +Die Benutzung der lokalen Datenbank erfordert im Idealfall ein einigermaßen +performantes Dateisystem. Ein Raspberry mit seiner langsamen SD-Karte ist in +diesem Fall sicherlich nicht die allerbeste Wahl. Das vollständige Update der +Datenbank dauert auf einem solchen System erfahrungsgemäß um die 15-20 Minuten. +Da dies aber im Hintergrund passiert, kann man unter Umständen gut damit leben. + +Das Addon wurde auf verschiedenen Plattformen unter Linux, MacOS, Windows und +LibreELEC bzw. OpenELEC getestet. Auch verschiedene Android Systeme konnten +schon erfolgreich getestet werden. Wegen der Vielzahl der Plattformen ist es +allerdings nicht möglich eine abschließende Kompatibilitätsaussage zu machen. Alternativ-Konfigurationen @@ -95,11 +86,13 @@ Datenbank (MySQL) zu nutzen. Da viele Kodi-Nutzer über ein eigenes NAS-System verfügen um ihre Medien dem Media-Center zur Verfügung zu stellen, eignet sich dieses in der Regel -auch als MySQL Datenbank-Server da nahezu alle NAS-Betriebssysteme die -Installation eines solchen anbieten. +auch als MySQL bzw. MariaDB Datenbank-Server da nahezu alle NAS-Betriebssysteme +die Installation eines solchen anbieten. -Hierfür muss lediglich die entsprechende Datenbank im MySQL Server mit -dem SQL-Skript `resources/sql/filmliste-mysql-v1.sql` erzeugt werden. +Ist das Addon so konfiguriert, dass eine MySQL/MariaDB Datenbank genutzt werden +soll, erzeugt dieses die Datenbank selbsttätig, falls diese auf dem +Datenbankserver noch nicht existiert. Der angegebene Datenabankbenutzer muss +dafür allerdings auch die Rechte besitzen. Die Verbindung zur Datenbank kann in den Addon-Einstellungen im Abschnitt _"Datenbank Einstellungen"_ vorgenommen werden. @@ -117,7 +110,7 @@ Standalone Datenbank Update Prozess Um die Datenbankaktualisierung von der Kommandozeile auszuführen, muss das Zielsystem einen python2-Interpreter bereitstellen. Des weiteren müssen noch folgende zwei Bibliotheken zur Verfügung stehen, sowie das Entpackprogramm -'xz': +'xz' (optional): * ijson * mysql-connector @@ -145,19 +138,18 @@ das Programm spezifische Hilfe aus. Beispiel: ```` leo@bookpoldo ~/plugin.video.mediathekview $ ./mvupdate mysql -h -usage: mvupdate mysql [-h] [-H HOST] [-u USER] [-p PASSWORD] [-d DATABASE] +usage: mvupdate mysql [-h] [-H HOST] [-P PORT] [-u USER] [-p PASSWORD] + [-d DATABASE] optional arguments: -h, --help show this help message and exit - -H HOST, --host HOST hostname or ip of the MySQL server (default: - localhost) - -u USER, --user USER username for the MySQL server connection (default: - filmliste) + -H HOST, --host HOST hostname or ip address (default: localhost) + -P PORT, --port PORT connection port (default: 3306) + -u USER, --user USER connection username (default: mediathekview) -p PASSWORD, --password PASSWORD - password for the MySQL server connection (default: - None) + connection password (default: None) -d DATABASE, --database DATABASE - MySQL database for mediathekview (default: filmliste) + database name (default: mediathekview) ```` @@ -216,22 +208,16 @@ After installation, the addon starts in local mode: this means that a local SQLite database is used, which is also updated locally by the Kodi system. This is probably the most common scenario. -* a file system with a decent performance for the database. A Raspberry with - its slow SD card is certainly not the very best choice in this case but still - acceptable. The full update will take in this case about 15-20 Minutes but - since this happens in the background, you may be able to live with it. -* The unpacker 'xz' on the Kodi system. To use the database updater, this - program must be installed on the system in one of the standard directories - (/bin, /usr/bin, /usr/local/bin). Under Windows or if the program is - installed in a different directory, the path to the program must be specified - in the addon settings. If the unpacker is not available on the target system, - the addon issues a message and disables the update process. +Ideally, using the local database requires a file system with a decent +performance. A Raspberry with a slow SD card is certainly not the very +best choice in this case but still acceptable. The full update will take +in this case about 15-20 Minutes but since this happens in the background, +you may be able to live with it. -The addon has been tested on different platforms under Linux, MacOS and -LibreELEC/OpenELEC. The corresponding unpacker was also available there. -Under Windows, the unpacker must be manually installed and its path must -be specified in the addon settings. Due to the lack of a test system, -however, this could not be tested at the present time. +The addon has been tested on different platforms under Linux, MacOS, +Windows and LibreELEC/OpenELEC. Various Android systems have also been +tested successfully. Due to the variety of platforms, however, it is not +possible to make a final compatibility statement. Alternate Configurations @@ -242,8 +228,13 @@ with a very slow SD card) or if the program 'xz' is missing, it is also possible to use the addon with an external database (MySQL). Since many Kodi users have their own NAS system to make their media available -to the media center, this is usually also suitable as a MySQL database server -since almost all NAS operating systems offer the installation of MySQL. +to the media center, this is usually also suitable as a MySQL/MariaDB database +server since almost all NAS operating systems offer the installation of MySQL. + +If the addon is configured to use a MySQL/MariaDB database, the database is +created automatically if it does not yet exist on the database server. However, +the specified database user must also have sufficient user rights in order to +do this. When you have a running MySQL server avaible, you have only to create the database by running the SQL script `resources/sql/filmliste-mysql-v1.sql`. @@ -289,19 +280,18 @@ the application shows specific help instructions: ```` leo@bookpoldo ~/plugin.video.mediathekview $ ./mvupdate mysql -h -usage: mvupdate mysql [-h] [-H HOST] [-u USER] [-p PASSWORD] [-d DATABASE] +usage: mvupdate mysql [-h] [-H HOST] [-P PORT] [-u USER] [-p PASSWORD] + [-d DATABASE] optional arguments: -h, --help show this help message and exit - -H HOST, --host HOST hostname or ip of the MySQL server (default: - localhost) - -u USER, --user USER username for the MySQL server connection (default: - filmliste) + -H HOST, --host HOST hostname or ip address (default: localhost) + -P PORT, --port PORT connection port (default: 3306) + -u USER, --user USER connection username (default: mediathekview) -p PASSWORD, --password PASSWORD - password for the MySQL server connection (default: - None) + connection password (default: None) -d DATABASE, --database DATABASE - MySQL database for mediathekview (default: filmliste) + database name (default: mediathekview) ```` @@ -344,25 +334,22 @@ Come funziona L'addon scarica il database da MediathekView e lo importa in un database SQLite locale o, in alternativa, in un database MySQL locale o remoto (per l'uso da -parte di più client Kodi). -Durante il runtime di Kodi, i file differenziali vengono scaricati da -MediathekView in un intervallo configurabile (predefinito: 2 ore) ed importati -nel database. Al più tardi entro il giorno successivo all'ultimo aggiornamento, -l'aggiornamento sarà nuovamente effettuato tramite l'aggiornamento completo -di Mediathekview. - -* Un file system con prestazioni accettabili per il database. Un Raspberry con - la sua lenta scheda SD non è certamente la miglior scelta ma sempre ancora - accettabile. La durata di un aggiornamento completo in questo caso sarà - intorno ai 15-20 minuti. Ma poiché questo accade in background, l'impatto - sarà essere accetabile. -* Il decompressore 'xz' sul sistema Kodi. Per utilizzare il programma di - aggiornamento del database, questo programma deve essere installato sul - sistema in una delle directory standard (/bin, /usr/bin, /usr/local/bin). In - Windows o se il programma è installato in una directory diversa, il percorso - del programma deve essere specificato nelle impostazioni dell'addon. Se il - decompressore non è disponibile per il sistema, l'addon mostra un messaggio - e disabilita il processo di aggiornamento. +parte di molteplici sistemi Kodi). Durante il runtime di Kodi, i file di +aggiornamento differenziali vengono scaricati da MediathekView in un intervallo +configurabile (predefinito: 2 ore) ed importati nel database. Al più tardi +entro il giorno successivo all'ultimo aggiornamento, l'aggiornamento sarà +nuovamente effettuato tramite l'aggiornamento completo di Mediathekview. + +Idealmente, l'utilizzo del database locale richiede un file system con +prestazioni accettabili. Un Raspberry di prima generazione con una scheda SD +lenta non è certamente la miglior scelta ma sempre ancora accettabile. La +durata di un aggiornamento completo in questo caso sarà intorno ai 15-20 +minuti. Ma poiché questo accade in background, l'impatto sarà accetabile. + +L'addon è stato testato su diverse piattaforme in Linux, MacOS, Windows e +LibreELEC nonchè OpenELEC. Anche diversi sistemi Android sono stati testati +con successo. A causa della varietà delle piattaforme, tuttavia, non è +possibile fare una dichiarazione finale di compatibilità. Configurazioni alternative @@ -374,11 +361,13 @@ Raspberry PI con una scheda SD molto lenta) o se manca il programma 'xz', Dal momento che molti utenti Kodi hanno il proprio sistema NAS per rendere i loro contenuti mediali disponibili al media center, questo è di solito anche -adatto come server di database MySQL, dal momento che quasi tutti i sistemi -operativi NAS offrono l'installazione di un tale database. +adatto come server di database MySQL/MariaDB, dal momento che quasi tutti i +sistemi operativi NAS offrono l'installazione di un tale database. -Dopodiche sarà sufficiente creare la banca dati mediante lo script SQL -disponibile in `resources/sql/filmliste-mysql-v1.sql`. +Se l' addon è configurato per utilizzare un database MySQL/MariaDB, il database +verrà creato automaticamente se non esiste ancora sul database server. +Tuttavia, anche l'utente del database specificato deve avere i diritti +necessari. Il collegamento al database può essere effettuato nelle impostazioni dell'addon nella sezione "Impostazioni Banca Dati". @@ -423,17 +412,16 @@ aggiornare, l'applicazione mostrerà le opzioni disponibili: ```` leo@bookpoldo ~/plugin.video.mediathekview $ ./mvupdate mysql -h -usage: mvupdate mysql [-h] [-H HOST] [-u USER] [-p PASSWORD] [-d DATABASE] +usage: mvupdate mysql [-h] [-H HOST] [-P PORT] [-u USER] [-p PASSWORD] + [-d DATABASE] optional arguments: -h, --help show this help message and exit - -H HOST, --host HOST hostname or ip of the MySQL server (default: - localhost) - -u USER, --user USER username for the MySQL server connection (default: - filmliste) + -H HOST, --host HOST hostname or ip address (default: localhost) + -P PORT, --port PORT connection port (default: 3306) + -u USER, --user USER connection username (default: mediathekview) -p PASSWORD, --password PASSWORD - password for the MySQL server connection (default: - None) + connection password (default: None) -d DATABASE, --database DATABASE - MySQL database for mediathekview (default: filmliste) + database name (default: mediathekview) ```` diff --git a/plugin.video.mediathekview/addon.py b/plugin.video.mediathekview/addon.py index 0c10b29..64e925d 100644 --- a/plugin.video.mediathekview/addon.py +++ b/plugin.video.mediathekview/addon.py @@ -27,21 +27,22 @@ from __future__ import unicode_literals # ,absolute_import, division # from future import standard_library # from builtins import * # standard_library.install_aliases() -import io,os,re,sys,urlparse,datetime,string,urllib,urllib2 -import xbmc,xbmcplugin,xbmcgui,xbmcaddon,xbmcvfs +import os,re,sys,urlparse,datetime +import xbmcplugin,xbmcgui,xbmcvfs -from de.yeasoft.kodi.KodiAddon import KodiPlugin -from de.yeasoft.kodi.KodiUI import KodiBGDialog +from contextlib import closing -from classes.store import Store -from classes.notifier import Notifier -from classes.settings import Settings -from classes.filmui import FilmUI -from classes.channelui import ChannelUI -from classes.initialui import InitialUI -from classes.showui import ShowUI -from classes.updater import MediathekViewUpdater -from classes.ttml2srt import ttml2srt +from resources.lib.kodi.KodiAddon import KodiPlugin +from resources.lib.kodi.KodiUI import KodiBGDialog + +from resources.lib.store import Store +from resources.lib.notifier import Notifier +from resources.lib.settings import Settings +from resources.lib.filmui import FilmUI +from resources.lib.channelui import ChannelUI +from resources.lib.initialui import InitialUI +from resources.lib.showui import ShowUI +from resources.lib.ttml2srt import ttml2srt # -- Classes ------------------------------------------------ class MediathekView( KodiPlugin ): @@ -52,9 +53,6 @@ class MediathekView( KodiPlugin ): self.notifier = Notifier() self.db = Store( self.getNewLogger( 'Store' ), self.notifier, self.settings ) - def __del__( self ): - del self.db - def showMainMenu( self ): # Search self.addFolderItem( 30901, { 'mode': "search" } ) @@ -79,6 +77,7 @@ class MediathekView( KodiPlugin ): if len( searchText ) > 2: self.db.Search( searchText, FilmUI( self ) ) else: + self.info( 'The following ERROR can be ignored. It is caused by the architecture of the Kodi Plugin Engine' ) self.endOfDirectory( False, cacheToDisc = True ) # self.showMainMenu() @@ -87,6 +86,7 @@ class MediathekView( KodiPlugin ): if len( searchText ) > 2: self.db.SearchFull( searchText, FilmUI( self ) ) else: + self.info( 'The following ERROR can be ignored. It is caused by the architecture of the Kodi Plugin Engine' ) self.endOfDirectory( False, cacheToDisc = True ) # self.showMainMenu() @@ -176,8 +176,8 @@ class MediathekView( KodiPlugin ): videourl = film.url_video # prepare names - showname = self._cleanup_filename( film.show )[:64] - filestem = self._cleanup_filename( film.title )[:64] + showname = mvutils.cleanup_filename( film.show )[:64] + filestem = mvutils.cleanup_filename( film.title )[:64] extension = os.path.splitext( videourl )[1] if not extension: extension = u'.mp4' @@ -211,7 +211,7 @@ class MediathekView( KodiPlugin ): bgd.Create( self.language( 30974 ), fileepi + extension ) try: bgd.Update( 0 ) - result = self._url_retrieve( videourl, movname, bgd.UrlRetrieveHook ) + result = mvutils.url_retrieve_vfs( videourl, movname, bgd.UrlRetrieveHook ) bgd.Close() if result is not None: self.notifier.ShowNotification( self.language( 30960 ), self.language( 30976 ).format( videourl ) ) @@ -226,7 +226,7 @@ class MediathekView( KodiPlugin ): bgd.Create( self.language( 30978 ), fileepi + u'.ttml' ) try: bgd.Update( 0 ) - result = self._url_retrieve( film.url_sub, ttmname, bgd.UrlRetrieveHook ) + result = mvutils.url_retrieve_vfs( film.url_sub, ttmname, bgd.UrlRetrieveHook ) try: ttml2srt( xbmcvfs.File( ttmname, 'r' ), xbmcvfs.File( srtname, 'w' ) ) except Exception as err: @@ -244,74 +244,43 @@ class MediathekView( KodiPlugin ): def doEnqueueFilm( self, filmid ): self.info( 'Enqueue {}', filmid ) - def _cleanup_filename( self, val ): - cset = string.letters + string.digits + u' _-#äöüÄÖÜßáàâéèêíìîóòôúùûÁÀÉÈÍÌÓÒÚÙçÇœ' - search = ''.join( [ c for c in val if c in cset ] ) - return search.strip() - def _make_nfo_files( self, film, episode, dirname, filename, videourl ): # create NFO files if not xbmcvfs.exists( dirname + 'tvshow.nfo' ): try: - file = xbmcvfs.File( dirname + 'tvshow.nfo', 'w' ) - file.write( bytearray( '\n', 'utf-8' ) ) - file.write( bytearray( '\n', 'utf-8' ) ) - file.write( bytearray( '\t{}\n'.format( film.show ), 'utf-8' ) ) - file.write( bytearray( '\t{}\n'.format( film.show ), 'utf-8' ) ) -# file.write( bytearray( '\t{}\n'.format( 2018 ), 'utf-8' ) ) # XXX TODO: That might be incorrect! - file.write( bytearray( '\t{}\n'.format( film.channel ), 'utf-8' ) ) - file.write( bytearray( '\n', 'utf-8' ) ) - file.close() + with closing( xbmcvfs.File( dirname + 'tvshow.nfo', 'w' ) ) as file: + file.write( b'\n' ) + file.write( b'\n' ) + file.write( bytearray( '\t{}\n'.format( film.show ), 'utf-8' ) ) + file.write( bytearray( '\t{}\n'.format( film.show ), 'utf-8' ) ) +# TODO: file.write( bytearray( '\t{}\n'.format( 2018 ), 'utf-8' ) ) + file.write( bytearray( '\t{}\n'.format( film.channel ), 'utf-8' ) ) + file.write( b'\n' ) except Exception as err: self.error( 'Failure creating show NFO file for {}: {}', videourl, err ) try: - file = xbmcvfs.File( filename, 'w' ) - file.write( bytearray( '\n', 'utf-8' ) ) - file.write( bytearray( '\t{}\n'.format( film.title ), 'utf-8' ) ) - file.write( bytearray( '\t1\n', 'utf-8' ) ) - file.write( bytearray( '\t{}\n'.format( episode ), 'utf-8' ) ) - file.write( bytearray( '\t{}\n'.format( film.show ), 'utf-8' ) ) - file.write( bytearray( '\t{}\n'.format( film.description ), 'utf-8' ) ) - file.write( bytearray( '\t{}\n'.format( film.aired ), 'utf-8' ) ) - if film.seconds > 60: - file.write( bytearray( '\t{}\n'.format( int( film.seconds / 60 ) ), 'utf-8' ) ) - file.write( bytearray( '\t{}\n', 'utf-8' ) ) - file.close() + with closing( xbmcvfs.File( filename, 'w' ) ) as file: + file.write( b'\n' ) + file.write( bytearray( '\t{}\n'.format( film.title ), 'utf-8' ) ) + file.write( b'\t1\n' ) + file.write( bytearray( '\t{}\n'.format( episode ), 'utf-8' ) ) + file.write( bytearray( '\t{}\n'.format( film.show ), 'utf-8' ) ) + file.write( bytearray( '\t{}\n'.format( film.description ), 'utf-8' ) ) + file.write( bytearray( '\t{}\n'.format( film.aired ), 'utf-8' ) ) + if film.seconds > 60: + file.write( bytearray( '\t{}\n'.format( int( film.seconds / 60 ) ), 'utf-8' ) ) + file.write( bytearray( '\t{}\n' ) except Exception as err: self.error( 'Failure creating episode NFO file for {}: {}', videourl, err ) - def _url_retrieve( self, videourl, filename, reporthook, chunk_size = 8192 ): - f = xbmcvfs.File( filename, 'wb' ) - u = urllib2.urlopen( videourl ) - - total_size = int( u.info().getheader( 'Content-Length' ).strip() ) if u.info() and u.info().getheader( 'Content-Length' ) else 0 - total_chunks = 0 - - while True: - reporthook( total_chunks, chunk_size, total_size ) - chunk = u.read( chunk_size ) - if not chunk: - break - f.write( chunk ) - total_chunks += 1 - f.close() - return ( filename, [], ) - def Init( self ): self.args = urlparse.parse_qs( sys.argv[2][1:] ) self.db.Init() if self.settings.HandleFirstRun(): # TODO: Implement Issue #16 pass - if MediathekViewUpdater( self.getNewLogger( 'Updater' ), self.notifier, self.settings ).PrerequisitesMissing(): - self.setSetting( 'updenabled', 'false' ) - self.settings.Reload() - xbmcgui.Dialog().textviewer( - self.language( 30963 ), - self.language( 30964 ) - ) def Do( self ): mode = self.args.get( 'mode', None ) @@ -327,7 +296,7 @@ class MediathekView( KodiPlugin ): channel = self.args.get( 'channel', [0] ) self.db.GetRecents( channel[0], FilmUI( self ) ) elif mode[0] == 'recentchannels': - self.db.GetRecentChannels( ChannelUI( self.addon_handle, next = 'recent' ) ) + self.db.GetRecentChannels( ChannelUI( self.addon_handle, nextdir = 'recent' ) ) elif mode[0] == 'channels': self.db.GetChannels( ChannelUI( self.addon_handle ) ) elif mode[0] == 'action-dbinfo': @@ -352,6 +321,7 @@ class MediathekView( KodiPlugin ): def Exit( self ): self.db.Exit() + # -- Main Code ---------------------------------------------- if __name__ == '__main__': addon = MediathekView() diff --git a/plugin.video.mediathekview/addon.xml b/plugin.video.mediathekview/addon.xml index 864bce7..596fe4b 100644 --- a/plugin.video.mediathekview/addon.xml +++ b/plugin.video.mediathekview/addon.xml @@ -1,7 +1,7 @@ @@ -19,13 +19,20 @@ library="service.py" start="startup" /> - Öffentlich-Rechtliche Mediatheken - Public service video-platforms - Piattaforme video dalle emittenti pubbliche - Ermöglicht den Zugriff auf fast alle deutschen Mediatheken der öffentlich Rechtlichen basierend auf der Datenbank von MediathekView.de - Gives access to most video-platforms from German public service broadcasters using the database of MediathekView.de - Fornisce l'accesso a gran parte delle piattaforme video operate dalle emittenti pubbliche tedesche usando la banca dati di MediathekView.de - v0.3.4 (2018-01-11): + Öffentlich-Rechtliche Mediatheken + Public service video-platforms + Piattaforme video dalle emittenti pubbliche + Ermöglicht den Zugriff auf fast alle deutschen Mediatheken der öffentlich Rechtlichen basierend auf der Datenbank von MediathekView.de + Gives access to most video-platforms from German public service broadcasters using the database of MediathekView.de + Fornisce l'accesso a gran parte delle piattaforme video operate dalle emittenti pubbliche tedesche usando la banca dati di MediathekView.de + v0.4.0 (2018-01-14): +- Android und Windows Kompatibilität da kein externer Entpacker mehr benötigt wird +- Der Datenbanktreiber für MySQL kann nun die Datenbank selbsttätig anlegen +- Im Datenbanktreiber für MySQL kann nun auch die Portnummer angegeben werden +- Konfigurierbare Begrenzung der Suchergebnisse +v0.3.5 (2018-01-11): +- Anpassungen an die Kodi-Addon Regeln +v0.3.4 (2018-01-11): - Die Suche sucht nun auch im Sendungs-Name - "Vor Kurzem hinzugefügt" nach Sendern durchsuchen - Deutsche und Italienische Übersetzung funktionieren nun @@ -53,7 +60,7 @@ v0.3.0 (2018-01-07): https://mediathekview.de/ info@mediathekview.de - resources/icon.jpg + resources/icon.png resources/fanart.jpg resources/screenshot1.png resources/screenshot2.png diff --git a/plugin.video.mediathekview/classes/__init__.py b/plugin.video.mediathekview/classes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugin.video.mediathekview/classes/channel.py b/plugin.video.mediathekview/classes/channel.py deleted file mode 100644 index d142383..0000000 --- a/plugin.video.mediathekview/classes/channel.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- 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 deleted file mode 100644 index 7061013..0000000 --- a/plugin.video.mediathekview/classes/channelui.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- 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 deleted file mode 100644 index 53d7f14..0000000 --- a/plugin.video.mediathekview/classes/exceptions.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- 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 deleted file mode 100644 index b417078..0000000 --- a/plugin.video.mediathekview/classes/film.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- 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 deleted file mode 100644 index eb2cfa8..0000000 --- a/plugin.video.mediathekview/classes/filmui.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- 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 deleted file mode 100644 index e9593d8..0000000 --- a/plugin.video.mediathekview/classes/initialui.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- 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 deleted file mode 100644 index f314679..0000000 --- a/plugin.video.mediathekview/classes/mvupdate.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- 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 deleted file mode 100644 index f76d059..0000000 --- a/plugin.video.mediathekview/classes/notifier.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- 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 deleted file mode 100644 index 58ea805..0000000 --- a/plugin.video.mediathekview/classes/settings.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- 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 deleted file mode 100644 index e0c9127..0000000 --- a/plugin.video.mediathekview/classes/show.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- 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 deleted file mode 100644 index b6f0bf8..0000000 --- a/plugin.video.mediathekview/classes/showui.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- 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 deleted file mode 100644 index 15c6ef2..0000000 --- a/plugin.video.mediathekview/classes/store.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- 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 deleted file mode 100644 index a272277..0000000 --- a/plugin.video.mediathekview/classes/storemysql.py +++ /dev/null @@ -1,534 +0,0 @@ -# -*- 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 deleted file mode 100644 index f73acdf..0000000 --- a/plugin.video.mediathekview/classes/storesqlite.py +++ /dev/null @@ -1,766 +0,0 @@ -# -*- 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 deleted file mode 100644 index e36b41e..0000000 --- a/plugin.video.mediathekview/classes/ttml2srt.py +++ /dev/null @@ -1,221 +0,0 @@ -# -*- 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 += '' % style['color'] - - if style.get('fontstyle') == 'italic': - result += '' - - 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 += '' - - if style.get('fontstyle') == 'italic': - result += '' - - 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 deleted file mode 100644 index 8609303..0000000 --- a/plugin.video.mediathekview/classes/updater.py +++ /dev/null @@ -1,431 +0,0 @@ -# -*- 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 diff --git a/plugin.video.mediathekview/de/__init__.py b/plugin.video.mediathekview/de/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugin.video.mediathekview/de/yeasoft/__init__.py b/plugin.video.mediathekview/de/yeasoft/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugin.video.mediathekview/de/yeasoft/base/Logger.py b/plugin.video.mediathekview/de/yeasoft/base/Logger.py deleted file mode 100644 index 7fad5fc..0000000 --- a/plugin.video.mediathekview/de/yeasoft/base/Logger.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Leo Moll and Dominik Schlösser -# - -# -- Imports ------------------------------------------------ - -# -- Classes ------------------------------------------------ -class Logger( object ): - def __init__( self, name, version, topic = None ): - self.name = name - self.version = version - self.setTopic( topic ) - - def getNewLogger( self, topic = None ): - pass - - def setTopic( self, topic = None ): - if topic == None: - self.prefix = '[%s-%s]: ' % ( self.name, self.version ) - else: - self.prefix = '[%s-%s:%s]: ' % ( self.name, self.version, topic ) - - def debug( self, message, *args ): - pass - - def info( self, message, *args ): - pass - - def warn( self, message, *args ): - pass - - def error( self, message, *args ): - pass diff --git a/plugin.video.mediathekview/de/yeasoft/base/__init__.py b/plugin.video.mediathekview/de/yeasoft/base/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugin.video.mediathekview/de/yeasoft/kodi/KodiAddon.py b/plugin.video.mediathekview/de/yeasoft/kodi/KodiAddon.py deleted file mode 100644 index 9d2fa04..0000000 --- a/plugin.video.mediathekview/de/yeasoft/kodi/KodiAddon.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Leo Moll and Dominik Schlösser -# - -# -- Imports ------------------------------------------------ -import os, sys, urllib -import xbmc, xbmcgui, xbmcaddon, xbmcplugin - -from de.yeasoft.kodi.KodiLogger import KodiLogger - -# -- Classes ------------------------------------------------ -class KodiAddon( KodiLogger ): - - def __init__( self ): - self.addon = xbmcaddon.Addon() - self.addon_id = self.addon.getAddonInfo( 'id' ) - self.icon = self.addon.getAddonInfo( 'icon' ) - self.fanart = self.addon.getAddonInfo( 'fanart' ) - self.version = self.addon.getAddonInfo( 'version' ) - self.path = self.addon.getAddonInfo( 'path' ) - self.datapath = os.path.join( xbmc.translatePath( "special://masterprofile" ).decode('utf-8'), 'addon_data', self.addon_id.decode('utf-8') ) - self.language = self.addon.getLocalizedString - KodiLogger.__init__( self, self.addon_id, self.version ) - - def getSetting( self, id ): - return self.addon.getSetting( id ) - - def setSetting( self, id, value ): - return self.addon.setSetting( id, value ) - - def doAction( self, action ): - xbmc.executebuiltin( 'Action({})'.format( action ) ) - -class KodiService( KodiAddon ): - def __init__( self ): - KodiAddon.__init__( self ) - -class KodiPlugin( KodiAddon ): - def __init__( self ): - KodiAddon.__init__( self ) - self.base_url = sys.argv[0] - self.addon_handle = int( sys.argv[1] ) - - def build_url( self, query ): - return self.base_url + '?' + urllib.urlencode( query ) - - def runPlugin( self, params ): - xbmc.executebuiltin( 'RunPlugin({})'.format( self.build_url( params ) ) ) - - def addActionItem( self, name, params ): - self.addDirectoryItem( name, params, False ) - - def addFolderItem( self, name, params ): - self.addDirectoryItem( name, params, True ) - - def addDirectoryItem( self, name, params, isFolder ): - if type( name ) is int: - name = self.language( name ) - li = xbmcgui.ListItem( name ) - xbmcplugin.addDirectoryItem( - handle = self.addon_handle, - url = self.build_url( params ), - listitem = li, - isFolder = isFolder - ) - - def endOfDirectory( self, succeeded = True, updateListing = False, cacheToDisc = True ): - xbmcplugin.endOfDirectory( self.addon_handle, succeeded, updateListing, cacheToDisc ) diff --git a/plugin.video.mediathekview/de/yeasoft/kodi/KodiLogger.py b/plugin.video.mediathekview/de/yeasoft/kodi/KodiLogger.py deleted file mode 100644 index f656b31..0000000 --- a/plugin.video.mediathekview/de/yeasoft/kodi/KodiLogger.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Leo Moll and Dominik Schlösser -# - -# -- Imports ------------------------------------------------ -import xbmc - -from de.yeasoft.base.Logger import Logger - -# -- Classes ------------------------------------------------ -class KodiLogger( Logger ): - - def __init__( self, name, version, topic = None ): - super( KodiLogger, self ).__init__( name, version, topic) - - def getNewLogger( self, topic = None ): - return KodiLogger( self.name, self.version, topic ) - - def debug( self, message, *args ): - self._log( xbmc.LOGDEBUG, message, *args ) - - def info( self, message, *args ): - self._log( xbmc.LOGNOTICE, message, *args ) - - def warn( self, message, *args ): - self._log( xbmc.LOGWARNING, message, *args ) - - def error( self, message, *args ): - self._log( xbmc.LOGERROR, 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 ) - xbmc.log( self.prefix + message.format( *parts ), level = level ) -# formatMessage = self._getFormatMessage( message ) -# xbmc.log( self.prefix + formatMessage.format( *parts ), level = level ) - -# def _getFormatMessage( self, message ): -# j = message.find( '{}' ) -# if j == -1: -# return message -# formatMessage = '' -# i = 0 -# index = 0 -# while j != -1: -# formatMessage += message[i:j] + '{' + str( index ) + '}' -# i = j + len( '{}' ) -# j = message.find( '{}', i ) -# index += 1 -# formatMessage += message[i:] -# return formatMessage diff --git a/plugin.video.mediathekview/de/yeasoft/kodi/KodiUI.py b/plugin.video.mediathekview/de/yeasoft/kodi/KodiUI.py deleted file mode 100644 index dee8dbb..0000000 --- a/plugin.video.mediathekview/de/yeasoft/kodi/KodiUI.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Leo Moll and Dominik Schlösser -# - -# -- Imports ------------------------------------------------ -import xbmc, xbmcgui - -# -- Classes ------------------------------------------------ -class KodiUI( object ): - - def __init__( self ): - self.bgdialog = None - - def GetEnteredText( self, deftext = '', heading = '', hidden = False ): - keyboard = xbmc.Keyboard( deftext, heading, 1 if hidden else 0 ) - keyboard.doModal() - if keyboard.isConfirmed(): - return keyboard.getText() - return deftext - - def ShowNotification( self, heading, message, icon = xbmcgui.NOTIFICATION_INFO, time = 5000, sound = True ): - xbmcgui.Dialog().notification( heading, message, icon, time, sound ) - - def ShowWarning( self, heading, message, time = 5000, sound = True ): - xbmcgui.Dialog().notification( heading, message, xbmcgui.NOTIFICATION_WARNING, time, sound ) - - def ShowError( self, heading, message, time = 5000, sound = True ): - xbmcgui.Dialog().notification( heading, message, xbmcgui.NOTIFICATION_ERROR, time, sound ) - - def ShowBGDialog( self, heading = None, message = None ): - if self.bgdialog is None: - self.bgdialog = xbmcgui.DialogProgressBG() - self.bgdialog.create( heading, message ) - else: - self.bgdialog.update( 0, heading, message ) - - def UpdateBGDialog( self, percent, heading = None, message = None ): - if self.bgdialog is not None: - self.bgdialog.update( percent, heading, message ) - - def CloseBGDialog( self ): - if self.bgdialog is not None: - self.bgdialog.close() - del self.bgdialog - self.bgdialog = None - -class KodiBGDialog( object ): - def __init__( self ): - self.bgdialog= None - - def __del__( self ): - self.Close() - - def Create( self, heading = None, message = None ): - if self.bgdialog is None: - self.bgdialog = xbmcgui.DialogProgressBG() - self.bgdialog.create( heading, message ) - else: - self.bgdialog.update( 0, heading, message ) - - def Update( self, percent, heading = None, message = None ): - if self.bgdialog is not None: - self.bgdialog.update( percent, heading, message ) - - def UrlRetrieveHook( self, blockcount, blocksize, totalsize ): - downloaded = blockcount * blocksize - if totalsize > 0: - percent = int( (downloaded * 100) / totalsize ) - if self.bgdialog is not None: - self.bgdialog.update( percent ) - - def Close( self ): - if self.bgdialog is not None: - self.bgdialog.close() - del self.bgdialog - self.bgdialog = None diff --git a/plugin.video.mediathekview/de/yeasoft/kodi/__init__.py b/plugin.video.mediathekview/de/yeasoft/kodi/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugin.video.mediathekview/resources/__init__.py b/plugin.video.mediathekview/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin.video.mediathekview/resources/fanart.jpg b/plugin.video.mediathekview/resources/fanart.jpg index 4d9bd9d..bc54726 100644 Binary files a/plugin.video.mediathekview/resources/fanart.jpg and b/plugin.video.mediathekview/resources/fanart.jpg differ diff --git a/plugin.video.mediathekview/resources/icon.jpg b/plugin.video.mediathekview/resources/icon.jpg deleted file mode 100644 index 79073be..0000000 Binary files a/plugin.video.mediathekview/resources/icon.jpg and /dev/null differ diff --git a/plugin.video.mediathekview/resources/icon.png b/plugin.video.mediathekview/resources/icon.png new file mode 100644 index 0000000..75403fe Binary files /dev/null and b/plugin.video.mediathekview/resources/icon.png differ diff --git a/plugin.video.mediathekview/resources/language/English/strings.po b/plugin.video.mediathekview/resources/language/English/strings.po deleted file mode 100644 index e4c3cf5..0000000 --- a/plugin.video.mediathekview/resources/language/English/strings.po +++ /dev/null @@ -1,300 +0,0 @@ -# Kodi Media Center language file -# Addon Name: MediathekView -# Addon id: plugin.video.mediathekview -# Addon Provider: leo.moll@yeasoft.com -msgid "" -msgstr "" -"Project-Id-Version: Kodi Addons\n" -"Report-Msgid-Bugs-To: leo.moll@yeasoft.com\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Alex\n" -"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -# Settings categories -msgctxt "#30001" -msgid "General" -msgstr "General" - -msgctxt "#30002" -msgid "Database Settings" -msgstr "Database Settings" - -# Settings Page 1 -msgctxt "#30110" -msgid "Prefer HD streams" -msgstr "Prefer HD streams" - -msgctxt "#30111" -msgid "No future videos (usually trailers)" -msgstr "No future videos (usually trailers)" - -msgctxt "#30112" -msgid "Minimum duration in minutes" -msgstr "Minimum duration in minutes" - -msgctxt "#30113" -msgid "Group shows of different channel" -msgstr "Group shows of different channel" - -msgctxt "#30114" -msgid "Download directory" -msgstr "Download directory" - -# Settings Page 2 -msgctxt "#30210" -msgid "Database type" -msgstr "Database type" - -msgctxt "#30211" -msgid "Database hostname" -msgstr "Database hostname" - -msgctxt "#30212" -msgid "Database username" -msgstr "Database username" - -msgctxt "#30213" -msgid "Database password" -msgstr "Database password" - -msgctxt "#30214" -msgid "Database name" -msgstr "Database name" - -msgctxt "#30221" -msgid "Internal (sqlite)" -msgstr "Internal (sqlite)" - -msgctxt "#30222" -msgid "External (mysql)" -msgstr "External (mysql)" - -msgctxt "#30231" -msgid "Database Updates" -msgstr "Database Updates" - -msgctxt "#30232" -msgid "Update interval in hours" -msgstr "Update interval in hours" - -msgctxt "#30233" -msgid "XZ Program Location" -msgstr "XZ Program Location" - -msgctxt "#30238" -msgid "Reset Database" -msgstr "Reset Database" - -# Main Menu -msgctxt "#30901" -msgid "Search in Title" -msgstr "Search in Title" - -msgctxt "#30902" -msgid "Search in Title and Description" -msgstr "Search in Title and Description" - -msgctxt "#30903" -msgid "Livestreams" -msgstr "Livestreams" - -msgctxt "#30904" -msgid "Recently Added" -msgstr "Recently Added" - -msgctxt "#30905" -msgid "Recently Added by Channel" -msgstr "Recently Added by Channel" - -msgctxt "#30906" -msgid "Browse by Show in all Channels" -msgstr "Browse by Show in all Channels" - -msgctxt "#30907" -msgid "Browse Shows by Channel" -msgstr "Browse Shows by Channel" - -msgctxt "#30908" -msgid "Database Information" -msgstr "Database Information" - -# Context Menu -msgctxt "#30921" -msgid "Download Video" -msgstr "Download Video" - -msgctxt "#30922" -msgid "Download LoRes Video" -msgstr "Download LoRes Video" - -msgctxt "#30923" -msgid "Download HD Video" -msgstr "Download HD Video" - -msgctxt "#30924" -msgid "Add to queue" -msgstr "Add to queue" - -# Database status -msgctxt "#30941" -msgid "Not Initialized" -msgstr "Not Initialized" - -msgctxt "#30942" -msgid "No Status Available" -msgstr "No Status Available" - -msgctxt "#30943" -msgid "Idle" -msgstr "Idle" - -msgctxt "#30944" -msgid "Updating..." -msgstr "Updating..." - -msgctxt "#30945" -msgid "Last Update Aborted" -msgstr "Last Update Aborted" - -# Other Strings -msgctxt "#30951" -msgid "Mediathek Database Error" -msgstr "Mediathek Database Error" - -msgctxt "#30952" -msgid "Download Error" -msgstr "Download Error" - -msgctxt "#30953" -msgid "Error while downloading {}: {}" -msgstr "Error while downloading {}: {}" - -msgctxt "#30954" -msgid "Required decompression program 'xz' not found on your system" -msgstr "Required decompression program 'xz' not found on your system" - -msgctxt "#30955" -msgid "Download Database Update" -msgstr "Download Database Update" - -msgctxt "#30956" -msgid "Mediathek Database Update" -msgstr "Mediathek Database Update" - -msgctxt "#30957" -msgid "Mediathek (%d): channels:%d, shows:%d, movies:%d ..." -msgstr "Mediathek (%d): channels:%d, shows:%d, movies:%d ..." - -msgctxt "#30958" -msgid "Please set a download folder in the settings" -msgstr "Please set a download folder in the settings" - -msgctxt "#30959" -msgid "Download directory could not be created: {}" -msgstr "Download directory could not be created: {}" - -msgctxt "#30960" -msgid "Download Successful" -msgstr "Download Successful" - -msgctxt "#30961" -msgid "Welcome to MediathekView" -msgstr "Welcome to MediathekView" - -msgctxt "#30962" -msgid "This Kodi addon allows access to most video-platforms from German public service " -"broadcasters using the database provided by the popular project of MediathekView.\n\n" -"Without the endless effort of the MediathekView contributors, addons like this would not " -"exist.\n\n" -"Please consider donating to this awesome project! Visit MediathekView at https://mediathekview.de/" -msgstr "This Kodi addon allows access to most video-platforms from German public service " -"broadcasters using the database provided by the great project of MediathekView.\n\n" -"Without the endless effort of the MediathekView contributors, addons like this would not " -"exist.\n\n" -"Please consider donating to this great project! Visit MediathekView at https://mediathekview.de/" - -msgctxt "#30963" -msgid "XZ Decompressor is Missing" -msgstr "XZ Decompressor is Missing" - -msgctxt "#30964" -msgid "The database updater needs the program 'xz' in order to process the database update files. " -"This programm was not found on your Kodi system.\n\nIf you want to use the database updater on this " -"system, you have to install 'xz' in one of the standard locations (/bin, /usr/bin, /usr/local/bin).\n\n" -"Alternatively you can specify the location of the 'xz' program in the addon settings.\n\n" -"Another possibilty would be the use of an external database updated either by another Kodi system " -"or by the provided update script running on a server (see README file).\n\n" -"The datebase updater has been disabled so you have to enable it again from the addon settings page." -msgstr "The database updater needs the program 'xz' in order to process the database update files. " -"This programm was not found on your Kodi system.\n\nIf you want to use the database updater on this " -"system, you have to install 'xz' in one of the standard locations (/bin, /usr/bin, /usr/local/bin).\n\n" -"Alternatively you can specify the location of the 'xz' program in the addon settings.\n\n" -"Another possibilty would be the use of an external database updated either by another Kodi system " -"or by the provided update script running on a server (see README file).\n\n" -"The datebase updater has been disabled so you have to enable it again from the addon settings page." - -msgctxt "#30965" -msgid "Database Status: %s" -msgstr "Database Status: %s" - -msgctxt "#30966" -msgid "This database was never updated" -msgstr "This database was never updated" - -msgctxt "#30967" -msgid "%s in progress...\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies" -msgstr "%s in progress...\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies" - -msgctxt "#30968" -msgid "%s in progress...\nAdditions: %d channels, %d shows, %d movies" -msgstr "%s in progress...\nAdditions: %d channels, %d shows, %d movies" - -msgctxt "#30969" -msgid "Last %s: %s\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" -msgstr "Last %s: %s\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" - -msgctxt "#30970" -msgid "Last %s: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" -msgstr "Last %s: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" - -msgctxt "#30971" -msgid "Total: %d channels, %d shows, %d movies" -msgstr "Total: %d channels, %d shows, %d movies" - -msgctxt "#30972" -msgid "Full Update" -msgstr "Full Update" - -msgctxt "#30973" -msgid "Differential Update" -msgstr "Differential Update" - -msgctxt "#30974" -msgid "Downloading Video" -msgstr "Downloading Video" - -msgctxt "#30975" -msgid "Error while downloading {}: {}" -msgstr "Error while downloading {}: {}" - -msgctxt "#30976" -msgid "Video {} has been downloaded" -msgstr "Video {} has been downloaded" - -msgctxt "#30977" -msgid "Video Already Exists" -msgstr "Video Already Exists" - -msgctxt "#30978" -msgid "Downloading Subtitles" -msgstr "Downloading Subtitles" - -msgctxt "#30979" -msgid "Download path does not exist" -msgstr "Download path does not exist" diff --git a/plugin.video.mediathekview/resources/language/German/strings.po b/plugin.video.mediathekview/resources/language/German/strings.po deleted file mode 100644 index c6da721..0000000 --- a/plugin.video.mediathekview/resources/language/German/strings.po +++ /dev/null @@ -1,302 +0,0 @@ -# Kodi Media Center language file -# Addon Name: MediathekView -# Addon id: plugin.video.mediathekview -# Addon Provider: leo.moll@yeasoft.com -msgid "" -msgstr "" -"Project-Id-Version: Kodi Addons\n" -"Report-Msgid-Bugs-To: leo.moll@yeasoft.com\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Alex\n" -"Language-Team: German (http://www.transifex.com/projects/p/xbmc-addons/language/de/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -# Settings categories -msgctxt "#30001" -msgid "General" -msgstr "Allgemein" - -msgctxt "#30002" -msgid "Database Settings" -msgstr "Datenbank Einstellungen" - -# Settings Page 1 -msgctxt "#30110" -msgid "Prefer HD streams" -msgstr "HD Streams bevorzugen" - -msgctxt "#30111" -msgid "No future videos (usually trailers)" -msgstr "Keine Videos aus der Zukunft (üblicherweise Trailer)" - -msgctxt "#30112" -msgid "Minimum duration in minutes" -msgstr "Minimlae Länge in Minuten" - -msgctxt "#30113" -msgid "Group shows of different channel" -msgstr "Sendungen verschiedener Sender zusammenfassen" - -msgctxt "#30114" -msgid "Download directory" -msgstr "Download-Verzeichnis" - -# Settings Page 2 -msgctxt "#30210" -msgid "Database type" -msgstr "Datenbank Typ" - -msgctxt "#30211" -msgid "Database hostname" -msgstr "Datenbank Hostname" - -msgctxt "#30212" -msgid "Database username" -msgstr "Datenbank Benutzername" - -msgctxt "#30213" -msgid "Database password" -msgstr "Datenbank Passwort" - -msgctxt "#30214" -msgid "Database name" -msgstr "Datenbank Name" - -msgctxt "#30221" -msgid "Internal (sqlite)" -msgstr "Intern (sqlite)" - -msgctxt "#30222" -msgid "External (mysql)" -msgstr "Extern (mysql)" - -msgctxt "#30231" -msgid "Database Updates" -msgstr "Datenbankupdates" - -msgctxt "#30232" -msgid "Update interval in hours" -msgstr "Aktualisierungsintervall in Stunden" - -msgctxt "#30233" -msgid "XZ Program Location" -msgstr "XZ Programm" - -msgctxt "#30238" -msgid "Reset Database" -msgstr "Datenbank zurücksetzen" - -# Main Menu -msgctxt "#30901" -msgid "Search in Title" -msgstr "Suchen in Titel" - -msgctxt "#30902" -msgid "Search in Title and Description" -msgstr "Suchen in Titel und Beschreibung" - -msgctxt "#30903" -msgid "Livestreams" -msgstr "Livestreams" - -msgctxt "#30904" -msgid "Recently Added" -msgstr "Vor Kurzem hinzugefügt" - -msgctxt "#30905" -msgid "Recently Added by Channel" -msgstr "Vor Kurzem hinzugefügt nach Sender" - -msgctxt "#30906" -msgid "Browse by Show in all Channels" -msgstr "Alle Sendungen" - -msgctxt "#30907" -msgid "Browse Shows by Channel" -msgstr "Alle Sendungen nach Sender" - -msgctxt "#30908" -msgid "Database Information" -msgstr "Datenbank Informationen" - -# Context Menu -msgctxt "#30921" -msgid "Download Video" -msgstr "Video herunterladen" - -msgctxt "#30922" -msgid "Download LoRes Video" -msgstr "Niedrig auflösendes Video herunterladen" - -msgctxt "#30923" -msgid "Download HD Video" -msgstr "Hoch auflösendes Video herunterladen" - -msgctxt "#30924" -msgid "Add to queue" -msgstr "Zur Warteschlange hinzufügen" - -# Database status -msgctxt "#30941" -msgid "Not Initialized" -msgstr "Nicht initialisiert" - -msgctxt "#30942" -msgid "No Status Available" -msgstr "Kein Status verfügbar" - -msgctxt "#30943" -msgid "Idle" -msgstr "Bereit" - -msgctxt "#30944" -msgid "Updating..." -msgstr "Aktualisierung läuft..." - -msgctxt "#30945" -msgid "Last Update Aborted" -msgstr "Letzte Aktualisierung abgebrochen" - -# Other Strings -msgctxt "#30951" -msgid "Mediathek Database Error" -msgstr "Mediathek Datenbank Fehler" - -msgctxt "#30952" -msgid "Download Error" -msgstr "Dowload Fehler" - -msgctxt "#30953" -msgid "Error while downloading {}: {}" -msgstr "Fehler beim herunterladen von {}: {}" - -msgctxt "#30954" -msgid "Required decompression program 'xz' not found on your system" -msgstr "Der benötigte Entpacker 'xz' konnte nicht gefunden werden" - -msgctxt "#30955" -msgid "Download Database Update" -msgstr "Datenbank Update wird heruntergeladen" - -msgctxt "#30956" -msgid "Mediathek Database Update" -msgstr "Mediathek wird aktualisiert" - -msgctxt "#30957" -msgid "Mediathek (%d): channels:%d, shows:%d, movies:%d ..." -msgstr "Mediathek (%d): Sender:%d, Sendungen:%d, Filme:%d ..." - -msgctxt "#30958" -msgid "Please set a download folder in the settings" -msgstr "Bitte waehlen Sie ein Download-Verzeichnis in den Einstellungen" - -msgctxt "#30959" -msgid "Download directory could not be created: {}" -msgstr "Fehler beim Ertzeugen des Download-Verzeichnisses: {}" - -msgctxt "#30960" -msgid "Download Successful" -msgstr "Download Erfolgreich" - -msgctxt "#30961" -msgid "Welcome to MediathekView" -msgstr "Willkommen" - -msgctxt "#30962" -msgid "This Kodi addon allows access to most video-platforms from German public service " -"broadcasters using the database provided by the popular project of MediathekView.\n\n" -"Without the endless effort of the MediathekView contributors, addons like this would not " -"exist.\n\n" -"Please consider donating to this awesome project! Visit MediathekView at https://mediathekview.de/" -msgstr "Dieses Kodi Addon Ermöglicht den Zugriff auf fast alle deutschen Mediatheken der " -"öffentlich Rechtlichen basierend auf der Datenbank des beliebten MediathekView Projektes.\n\n" -"Ohne den unermüdlichen Einsatz des MediathekView-Teams wäre ein solches Addon nicht möglich.\n\n" -"Um das Projekt zu unterstützen, besteht die Möglichkeit auf https://mediathekview.de/ eine " -"Spende zu hinterlassen." - -msgctxt "#30963" -msgid "XZ Decompressor is Missing" -msgstr "Entpacker XZ nicht gefunden" - -msgctxt "#30964" -msgid "The database updater needs the program 'xz' in order to process the database update files. " -"This programm was not found on your Kodi system.\n\nIf you want to use the database er on this " -"system, you have to install 'xz' in one of the standard locations (/bin, /usr/bin, /usr/local/bin).\n\n" -"Alternatively you can specify the location of the 'xz' program in the addon settings.\n\n" -"Another possibilty would be the use of an external database updated either by another Kodi system " -"or by the provided update script running on a server (see README file).\n\n" -"The datebase er has been disabled so you have to enable it again from the addon settings page." -msgstr "Der Datenbank-Aktualisierer benötigt den Entpacker 'xz' der auf diesem Kodi-System leider " -"nicht gefunden wurde.\n\nUm den Datenbank-Aktualisierer zu benutzen, muss dieses Programm auf dem " -"System in einem der Standard-Verzeichnisse (/bin, /usr/bin, /usr/local/bin) installiert werden.\n\n" -"Unter Windows bzw. falls das Programm in einem anderen Verzeichnis installiert ist, muss der Pfad " -"zum Programm in den Addon-Einstellungen angegeben werden.\n\n" -"Eine andere Möglichkeit ist die Benutzung einer externen Datenbank die entweder über eine anderes " -"Kodi-System oder über das mitgelieferte Update-Skript (siehe README) regelmäßig aktualisiert wird.\n\n" -"Der Datenbank-Aktualisierer wurde deaktiviert. Er kann über die Addon-Einstellungen wieder aktiviert " -"werden." - -msgctxt "#30965" -msgid "Database Status: %s" -msgstr "Datenbank-Status: %s" - -msgctxt "#30966" -msgid "This database was never updated" -msgstr "Diese Datenbank wurde noch nicht aktualisiert" - -msgctxt "#30967" -msgid "%s in progress...\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies" -msgstr "%s läuft...\nFilmdatenbank vom %s\nNeuzugänge: %d Sender, %d Sendungen, %d Filme" - -msgctxt "#30968" -msgid "%s in progress...\nAdditions: %d channels, %d shows, %d movies" -msgstr "%s läuft...\nNeuzugänge: %d Sender, %d Sendungen, %d Filme" - -msgctxt "#30969" -msgid "Last %s: %s\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" -msgstr "Letzte %s: %s\nFilmdatenbank vom %s\nNeuzugänge: %d Sender, %d Sendungen, %d Filme\nLöschungen: %d Sender, %d Sendungen, %d Filme" - -msgctxt "#30970" -msgid "Last %s: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" -msgstr "Letzte %s: %s\nNeuzugänge: %d Sender, %d Sendungen, %d Filme\nLöschungen: %d Sender, %d Sendungen, %d Filme" - -msgctxt "#30971" -msgid "Total: %d channels, %d shows, %d movies" -msgstr "Summen: %d Sender, %d Sendungen, %d Filme" - -msgctxt "#30972" -msgid "Full Update" -msgstr "Vollständige Aktualisierung" - -msgctxt "#30973" -msgid "Differential Update" -msgstr "Differentielle Aktualisierung" - -msgctxt "#30974" -msgid "Downloading Video" -msgstr "Video wird herunterladen" - -msgctxt "#30975" -msgid "Error while downloading {}: {}" -msgstr "Fehler beim Herunterladen von {}: {}" - -msgctxt "#30976" -msgid "Video {} has been downloaded" -msgstr "Video {} wurde heruntergeladen" - -msgctxt "#30977" -msgid "Video Already Exists" -msgstr "Das Video existiert bereits" - -msgctxt "#30978" -msgid "Downloading Subtitles" -msgstr "Untertitel werden heruntergeladen" - -msgctxt "#30979" -msgid "Download path does not exist" -msgstr "Download-Verzeichnis existiert nicht" diff --git a/plugin.video.mediathekview/resources/language/Italian/strings.po b/plugin.video.mediathekview/resources/language/Italian/strings.po deleted file mode 100644 index aadc963..0000000 --- a/plugin.video.mediathekview/resources/language/Italian/strings.po +++ /dev/null @@ -1,303 +0,0 @@ -# Kodi Media Center language file -# Addon Name: MediathekView -# Addon id: plugin.video.mediathekview -# Addon Provider: leo.moll@yeasoft.com -msgid "" -msgstr "" -"Project-Id-Version: Kodi Addons\n" -"Report-Msgid-Bugs-To: leo.moll@yeasoft.com\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Alex\n" -"Language-Team: Italian (http://www.transifex.com/projects/p/xbmc-addons/language/it/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: en\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -# Settings categories -msgctxt "#30001" -msgid "General" -msgstr "Visualizzazione" - -msgctxt "#30002" -msgid "Database Settings" -msgstr "Impostazioni Banca Dati" - -# Settings Page 1 -msgctxt "#30110" -msgid "Prefer HD streams" -msgstr "Preferisci alta definizione" - -msgctxt "#30111" -msgid "No future videos (usually trailers)" -msgstr "Non mostrare video futuri (usualmente trailer)" - -msgctxt "#30112" -msgid "Minimum duration in minutes" -msgstr "Durata minima in minuti" - -msgctxt "#30113" -msgid "Group shows of different channel" -msgstr "Raggruppa trasmissioni su canali diversi" - -msgctxt "#30114" -msgid "Download directory" -msgstr "Directory di download" - -# Settings Page 2 -msgctxt "#30210" -msgid "Database type" -msgstr "Tipo banca dati" - -msgctxt "#30211" -msgid "Database hostname" -msgstr "Hostname banca dati" - -msgctxt "#30212" -msgid "Database username" -msgstr "Nome utente" - -msgctxt "#30213" -msgid "Database password" -msgstr "Password" - -msgctxt "#30214" -msgid "Database name" -msgstr "Nome Database" - -msgctxt "#30221" -msgid "Internal (sqlite)" -msgstr "Interna (sqlite)" - -msgctxt "#30222" -msgid "External (mysql)" -msgstr "Esterna (mysql)" - -msgctxt "#30231" -msgid "Database Updates" -msgstr "Attualizzazione Database" - -msgctxt "#30232" -msgid "Update interval in hours" -msgstr "Intervallo di attualizzazione in ore" - -msgctxt "#30233" -msgid "XZ Program Location" -msgstr "Programma XZ" - -msgctxt "#30238" -msgid "Reset Database" -msgstr "Ripristina Database" - -# Main Menu -msgctxt "#30901" -msgid "Search in Title" -msgstr "Ricerca nel Titolo" - -msgctxt "#30902" -msgid "Search in Title and Description" -msgstr "Ricerca nel Titolo e nella Descrizione" - -msgctxt "#30903" -msgid "Livestreams" -msgstr "Livestreams" - -msgctxt "#30904" -msgid "Recently Added" -msgstr "Recenti" - -msgctxt "#30905" -msgid "Recently Added by Channel" -msgstr "Recenti secondo emittenti" - -msgctxt "#30906" -msgid "Browse by Show in all Channels" -msgstr "Tutte le trasmissioni" - -msgctxt "#30907" -msgid "Browse Shows by Channel" -msgstr "Trasmissioni secondo emittenti" - -msgctxt "#30908" -msgid "Database Information" -msgstr "Informazioni Database" - -# Context Menu -msgctxt "#30921" -msgid "Download Video" -msgstr "Scarica video" - -msgctxt "#30922" -msgid "Download LoRes Video" -msgstr "Scarica video bassa risoluzione" - -msgctxt "#30923" -msgid "Download HD Video" -msgstr "Scarica video alta risoluzione" - -msgctxt "#30924" -msgid "Add to queue" -msgstr "Metti in coda" - -# Database status -msgctxt "#30941" -msgid "Not Initialized" -msgstr "Non inizializzato" - -msgctxt "#30942" -msgid "No Status Available" -msgstr "Stato non disponibile" - -msgctxt "#30943" -msgid "Idle" -msgstr "Disponibile" - -msgctxt "#30944" -msgid "Updating..." -msgstr "Attualizzazione in corso..." - -msgctxt "#30945" -msgid "Last Update Aborted" -msgstr "Ultima attualizzazione interrotta" - -# Other Strings -msgctxt "#30951" -msgid "Mediathek Database Error" -msgstr "Errore Database Mediathek" - -msgctxt "#30952" -msgid "Download Error" -msgstr "Errore di Scaricamento" - -msgctxt "#30953" -msgid "Error while downloading {}: {}" -msgstr "Errore scaricando {0}: {1}" - -msgctxt "#30954" -msgid "Required decompression program 'xz' not found on your system" -msgstr "Il decompressore 'xz' manca sul sistema" - -msgctxt "#30955" -msgid "Download Database Update" -msgstr "Attualizzazione viene scaricata" - -msgctxt "#30956" -msgid "Mediathek Database Update" -msgstr "Mediathek in attualizzazione" - -msgctxt "#30957" -msgid "Mediathek (%d): channels:%d, shows:%d, movies:%d ..." -msgstr "Mediathek (%d): canali:%d, programmi:%d, film:%d ..." - -msgctxt "#30958" -msgid "Please set a download folder in the settings" -msgstr "Selezionare una directory di download nelle impostazioni" - -msgctxt "#30959" -msgid "Download directory could not be created: {}" -msgstr "Non è stato possibile creare la directory di download: {}" - -msgctxt "#30960" -msgid "Download Successful" -msgstr "Download Completato" - -msgctxt "#30961" -msgid "Welcome to MediathekView" -msgstr "Benvenuti" - -msgctxt "#30962" -msgid "This Kodi addon allows access to most video-platforms from German public service " -"broadcasters using the database provided by the popular project of MediathekView.\n\n" -"Without the endless effort of the MediathekView contributors, addons like this would not " -"exist.\n\n" -"Please consider donating to this awesome project! Visit MediathekView at https://mediathekview.de/" -msgstr "Questo addon di Kodi permette l'accesso a gran parte delle piattaforme video operate dalle " -"emittenti pubbliche tedesche usando la banca dati del grandioso progetto MediathekView.\n\n" -"Senza l'assiduo impegno del team di MediathekView un addon come questo non sarebba mai " -"stato fattibile.\n\n" -"Per aiutare i programmatori ad offrire continui aggiornamente al database, è possibile " -"donare un piccolo contributo sulla pagina del progetto https://mediathekview.de/" - -msgctxt "#30963" -msgid "XZ Decompressor is Missing" -msgstr "Decompressore XZ non trovato" - -msgctxt "#30964" -msgid "The database updater needs the program 'xz' in order to process the database update files. " -"This programm was not found on your Kodi system.\n\nIf you want to use the database er on this " -"system, you have to install 'xz' in one of the standard locations (/bin, /usr/bin, /usr/local/bin).\n\n" -"Alternatively you can specify the location of the 'xz' program in the addon settings.\n\n" -"Another possibilty would be the use of an external database updated either by another Kodi system " -"or by the provided update script running on a server (see README file).\n\n" -"The datebase er has been disabled so you have to enable it again from the addon settings page." -msgstr "L'attualizzatore del database necessita il programma di decompressione 'xz' che purtroppo " -"non è stato trovato su questo sistema Kodi.\n\nSe si vuole usare l'attualizzatore del database su " -"questo sistema, è necessario installare il programma di decompressione in una delle directory " -"standard (/bin, /usr/bin, /usr/local/bin).\n\nSu sistemi Windows o nel caso il programma di " -"decompressione sia installato in una directory alternativa, il percorso potrà essere specificato " -"nelle impostazioni dell'addon.\n\n" -"In alternativa è possibile utilizzare un database esterno attualizzato da un altro sistema Kodi " -"o dallo script in allegato operato su un server unix (vedi file di README).\n\n" -"L'attualizzatore è stato disabilitato e potrà essere riattivato nelle impostazione dell'addon." - -msgctxt "#30965" -msgid "Database Status: %s" -msgstr "Stato del Database: %s" - -msgctxt "#30966" -msgid "This database was never updated" -msgstr "Questo database non è stato mai attualizzato" - -msgctxt "#30967" -msgid "%s in progress...\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies" -msgstr "%s in corso...\nAggiornamento del %s\nAggiunzioni: %d canali, %d programmi, %d film" - -msgctxt "#30968" -msgid "%s in progress...\nAdditions: %d channels, %d shows, %d movies" -msgstr "%s in corso...\nAggiunzioni: %d canali, %d programmi, %d film" - -msgctxt "#30969" -msgid "Last %s: %s\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" -msgstr "Ultimo %s: %s\nAggiornamento del %s\nAggiunzioni: %d canali, %d programmi, %d film\nCancellazioni: %d canali, %d programmi, %d film" - -msgctxt "#30970" -msgid "Last %s: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" -msgstr "Ultimo %s: %s\nAggiunzioni: %d canali, %d programmi, %d film\nCancellazioni: %d canali, %d programmi, %d film" - -msgctxt "#30971" -msgid "Total: %d channels, %d shows, %d movies" -msgstr "Somme: %d canali, %d programmi, %d film" - -msgctxt "#30972" -msgid "Full Update" -msgstr "Aggiornamento Completo" - -msgctxt "#30973" -msgid "Differential Update" -msgstr "Aggiornamento Differenziale" - -msgctxt "#30974" -msgid "Downloading Video" -msgstr "Scaricamento video" - -msgctxt "#30975" -msgid "Error while downloading {}: {}" -msgstr "Errore durante lo scaricamento di {}: {}" - -msgctxt "#30976" -msgid "Video {} has been downloaded" -msgstr "Il video {} è stato scaricato" - -msgctxt "#30977" -msgid "Video Already Exists" -msgstr "Il video è già esistente" - -msgctxt "#30978" -msgid "Downloading Subtitles" -msgstr "Scaricamento sottotitoli" - -msgctxt "#30979" -msgid "Download path does not exist" -msgstr "Directory di scaricamento non esiste" diff --git a/plugin.video.mediathekview/resources/language/resource.language.de_de/strings.po b/plugin.video.mediathekview/resources/language/resource.language.de_de/strings.po new file mode 100644 index 0000000..d3832b3 --- /dev/null +++ b/plugin.video.mediathekview/resources/language/resource.language.de_de/strings.po @@ -0,0 +1,310 @@ +# Kodi Media Center language file +# Addon Name: MediathekView +# Addon id: plugin.video.mediathekview +# Addon Provider: leo.moll@yeasoft.com +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: leo.moll@yeasoft.com\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Alex\n" +"Language-Team: German (http://www.transifex.com/projects/p/xbmc-addons/language/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings categories +msgctxt "#30001" +msgid "General" +msgstr "Allgemein" + +msgctxt "#30002" +msgid "Database Settings" +msgstr "Datenbank Einstellungen" + +# Settings Page 1 +msgctxt "#30110" +msgid "Prefer HD streams" +msgstr "HD Streams bevorzugen" + +msgctxt "#30111" +msgid "No future videos (usually trailers)" +msgstr "Keine Videos aus der Zukunft (üblicherweise Trailer)" + +msgctxt "#30112" +msgid "Minimum duration in minutes" +msgstr "Minimlae Länge in Minuten" + +msgctxt "#30113" +msgid "Group shows of different channel" +msgstr "Sendungen verschiedener Sender zusammenfassen" + +msgctxt "#30114" +msgid "Download directory" +msgstr "Suchergebnisse eingrenzen" + +msgctxt "#30115" +msgid "Download directory" +msgstr "Download-Verzeichnis" + +# Settings Page 2 +msgctxt "#30210" +msgid "Database type" +msgstr "Datenbank Typ" + +msgctxt "#30211" +msgid "Database hostname" +msgstr "Datenbank Hostname" + +msgctxt "#30212" +msgid "Database port" +msgstr "Datenbank Portnummer" + +msgctxt "#30213" +msgid "Database username" +msgstr "Datenbank Benutzername" + +msgctxt "#30214" +msgid "Database password" +msgstr "Datenbank Passwort" + +msgctxt "#30215" +msgid "Database name" +msgstr "Datenbank Name" + +msgctxt "#30221" +msgid "Internal (sqlite)" +msgstr "Intern (sqlite)" + +msgctxt "#30222" +msgid "External (mysql)" +msgstr "Extern (mysql)" + +msgctxt "#30231" +msgid "Database Updates" +msgstr "Datenbankupdates" + +msgctxt "#30232" +msgid "Update interval in hours" +msgstr "Aktualisierungsintervall in Stunden" + +# Main Menu +msgctxt "#30901" +msgid "Search in Title" +msgstr "Suchen in Titel" + +msgctxt "#30902" +msgid "Search in Title and Description" +msgstr "Suchen in Titel und Beschreibung" + +msgctxt "#30903" +msgid "Livestreams" +msgstr "Livestreams" + +msgctxt "#30904" +msgid "Recently Added" +msgstr "Vor Kurzem hinzugefügt" + +msgctxt "#30905" +msgid "Recently Added by Channel" +msgstr "Vor Kurzem hinzugefügt nach Sender" + +msgctxt "#30906" +msgid "Browse by Show in all Channels" +msgstr "Alle Sendungen" + +msgctxt "#30907" +msgid "Browse Shows by Channel" +msgstr "Alle Sendungen nach Sender" + +msgctxt "#30908" +msgid "Database Information" +msgstr "Datenbank Informationen" + +# Context Menu +msgctxt "#30921" +msgid "Download Video" +msgstr "Video herunterladen" + +msgctxt "#30922" +msgid "Download LoRes Video" +msgstr "SD Video herunterladen" + +msgctxt "#30923" +msgid "Download HD Video" +msgstr "HD Video herunterladen" + +msgctxt "#30924" +msgid "Add to queue" +msgstr "Zur Warteschlange hinzufügen" + +# Database status +msgctxt "#30941" +msgid "Not Initialized" +msgstr "Nicht initialisiert" + +msgctxt "#30942" +msgid "No Status Available" +msgstr "Kein Status verfügbar" + +msgctxt "#30943" +msgid "Idle" +msgstr "Bereit" + +msgctxt "#30944" +msgid "Updating..." +msgstr "Aktualisierung läuft..." + +msgctxt "#30945" +msgid "Last Update Aborted" +msgstr "Letzte Aktualisierung abgebrochen" + +# Other Strings +msgctxt "#30951" +msgid "Mediathek Database Error" +msgstr "Mediathek Datenbank Fehler" + +msgctxt "#30952" +msgid "Download Error" +msgstr "Dowload Fehler" + +msgctxt "#30953" +msgid "Error while downloading {}: {}" +msgstr "Fehler beim herunterladen von {}: {}" + +msgctxt "#30954" +msgid "Required decompression program 'xz' not found on your system" +msgstr "Der benötigte Entpacker 'xz' konnte nicht gefunden werden" + +msgctxt "#30955" +msgid "Download Database Update" +msgstr "Datenbank Update wird heruntergeladen" + +msgctxt "#30956" +msgid "Mediathek Database Update" +msgstr "Mediathek wird aktualisiert" + +msgctxt "#30957" +msgid "Mediathek (%d): channels:%d, shows:%d, movies:%d ..." +msgstr "Mediathek (%d): Sender:%d, Sendungen:%d, Filme:%d ..." + +msgctxt "#30958" +msgid "Please set a download folder in the settings" +msgstr "Bitte waehlen Sie ein Download-Verzeichnis in den Einstellungen" + +msgctxt "#30959" +msgid "Download directory could not be created: {}" +msgstr "Fehler beim Ertzeugen des Download-Verzeichnisses: {}" + +msgctxt "#30960" +msgid "Download Successful" +msgstr "Download Erfolgreich" + +msgctxt "#30961" +msgid "Welcome to MediathekView" +msgstr "Willkommen" + +msgctxt "#30962" +msgid "This Kodi addon allows access to most video-platforms from German public service " +"broadcasters using the database provided by the popular project of MediathekView.\n\n" +"Without the endless effort of the MediathekView contributors, addons like this would not " +"exist.\n\n" +"Please consider donating to this awesome project! Visit MediathekView at https://mediathekview.de/" +msgstr "Dieses Kodi Addon Ermöglicht den Zugriff auf fast alle deutschen Mediatheken der " +"öffentlich Rechtlichen basierend auf der Datenbank des beliebten MediathekView Projektes.\n\n" +"Ohne den unermüdlichen Einsatz des MediathekView-Teams wäre ein solches Addon nicht möglich.\n\n" +"Um das Projekt zu unterstützen, besteht die Möglichkeit auf https://mediathekview.de/ eine " +"Spende zu hinterlassen." + +msgctxt "#30963" +msgid "No Decompressor Available" +msgstr "Kein Entpacker Verfügbar" + +msgctxt "#30964" +msgid "The database updater needs the program 'xz' in order to process the database update files. " +"This programm was not found on your Kodi system.\n\nIf you want to use the database er on this " +"system, you have to install 'xz' in one of the standard locations (/bin, /usr/bin, /usr/local/bin).\n\n" +"Alternatively you can specify the location of the 'xz' program in the addon settings.\n\n" +"Another possibilty would be the use of an external database updated either by another Kodi system " +"or by the provided update script running on a server (see README file).\n\n" +"The datebase er has been disabled so you have to enable it again from the addon settings page." +msgstr "Der Datenbank-Aktualisierer benötigt den Entpacker 'xz' der auf diesem Kodi-System leider " +"nicht gefunden wurde.\n\nUm den Datenbank-Aktualisierer zu benutzen, muss dieses Programm auf dem " +"System in einem der Standard-Verzeichnisse (/bin, /usr/bin, /usr/local/bin) installiert werden.\n\n" +"Unter Windows bzw. falls das Programm in einem anderen Verzeichnis installiert ist, muss der Pfad " +"zum Programm in den Addon-Einstellungen angegeben werden.\n\n" +"Eine andere Möglichkeit ist die Benutzung einer externen Datenbank die entweder über eine anderes " +"Kodi-System oder über das mitgelieferte Update-Skript (siehe README) regelmäßig aktualisiert wird.\n\n" +"Der Datenbank-Aktualisierer wurde deaktiviert. Er kann über die Addon-Einstellungen wieder aktiviert " +"werden." + +msgctxt "#30965" +msgid "Database Status: %s" +msgstr "Datenbank-Status: %s" + +msgctxt "#30966" +msgid "This database was never updated" +msgstr "Diese Datenbank wurde noch nicht aktualisiert" + +msgctxt "#30967" +msgid "%s in progress...\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies" +msgstr "%s läuft...\nFilmdatenbank vom %s\nNeuzugänge: %d Sender, %d Sendungen, %d Filme" + +msgctxt "#30968" +msgid "%s in progress...\nAdditions: %d channels, %d shows, %d movies" +msgstr "%s läuft...\nNeuzugänge: %d Sender, %d Sendungen, %d Filme" + +msgctxt "#30969" +msgid "Last %s: %s\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" +msgstr "Letzte %s: %s\nFilmdatenbank vom %s\nNeuzugänge: %d Sender, %d Sendungen, %d Filme\nLöschungen: %d Sender, %d Sendungen, %d Filme" + +msgctxt "#30970" +msgid "Last %s: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" +msgstr "Letzte %s: %s\nNeuzugänge: %d Sender, %d Sendungen, %d Filme\nLöschungen: %d Sender, %d Sendungen, %d Filme" + +msgctxt "#30971" +msgid "Total: %d channels, %d shows, %d movies" +msgstr "Summen: %d Sender, %d Sendungen, %d Filme" + +msgctxt "#30972" +msgid "Full Update" +msgstr "Vollständige Aktualisierung" + +msgctxt "#30973" +msgid "Differential Update" +msgstr "Differentielle Aktualisierung" + +msgctxt "#30974" +msgid "Downloading Video" +msgstr "Video wird heruntergeladen" + +msgctxt "#30975" +msgid "Error while downloading {}: {}" +msgstr "Fehler beim Herunterladen von {}: {}" + +msgctxt "#30976" +msgid "Video {} has been downloaded" +msgstr "Video {} wurde heruntergeladen" + +msgctxt "#30977" +msgid "Video Already Exists" +msgstr "Das Video existiert bereits" + +msgctxt "#30978" +msgid "Downloading Subtitles" +msgstr "Untertitel werden heruntergeladen" + +msgctxt "#30979" +msgid "Download path does not exist" +msgstr "Download-Verzeichnis existiert nicht" + +msgctxt "#30980" +msgid "Results Limited" +msgstr "Ergebnisse Beschränkt" + +msgctxt "#30981" +msgid "Only the first {} results are shown." +msgstr "Es werden nur die ersten {} Ergebnisse angezeigt." diff --git a/plugin.video.mediathekview/resources/language/resource.language.en_gb/strings.po b/plugin.video.mediathekview/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 0000000..5c121e8 --- /dev/null +++ b/plugin.video.mediathekview/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,308 @@ +# Kodi Media Center language file +# Addon Name: MediathekView +# Addon id: plugin.video.mediathekview +# Addon Provider: leo.moll@yeasoft.com +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: leo.moll@yeasoft.com\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Alex\n" +"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings categories +msgctxt "#30001" +msgid "General" +msgstr "General" + +msgctxt "#30002" +msgid "Database Settings" +msgstr "Database Settings" + +# Settings Page 1 +msgctxt "#30110" +msgid "Prefer HD streams" +msgstr "Prefer HD streams" + +msgctxt "#30111" +msgid "No future videos (usually trailers)" +msgstr "No future videos (usually trailers)" + +msgctxt "#30112" +msgid "Minimum duration in minutes" +msgstr "Minimum duration in minutes" + +msgctxt "#30113" +msgid "Group shows of different channel" +msgstr "Group shows of different channel" + +msgctxt "#30114" +msgid "Limit search results" +msgstr "Limit search results" + +msgctxt "#30115" +msgid "Download directory" +msgstr "Download directory" + +# Settings Page 2 +msgctxt "#30210" +msgid "Database type" +msgstr "Database type" + +msgctxt "#30211" +msgid "Database hostname" +msgstr "Database hostname" + +msgctxt "#30212" +msgid "Database port" +msgstr "Database port" + +msgctxt "#30213" +msgid "Database username" +msgstr "Database username" + +msgctxt "#30214" +msgid "Database password" +msgstr "Database password" + +msgctxt "#30215" +msgid "Database name" +msgstr "Database name" + +msgctxt "#30221" +msgid "Internal (sqlite)" +msgstr "Internal (sqlite)" + +msgctxt "#30222" +msgid "External (mysql)" +msgstr "External (mysql)" + +msgctxt "#30231" +msgid "Database Updates" +msgstr "Database Updates" + +msgctxt "#30232" +msgid "Update interval in hours" +msgstr "Update interval in hours" + +# Main Menu +msgctxt "#30901" +msgid "Search in Title" +msgstr "Search in Title" + +msgctxt "#30902" +msgid "Search in Title and Description" +msgstr "Search in Title and Description" + +msgctxt "#30903" +msgid "Livestreams" +msgstr "Livestreams" + +msgctxt "#30904" +msgid "Recently Added" +msgstr "Recently Added" + +msgctxt "#30905" +msgid "Recently Added by Channel" +msgstr "Recently Added by Channel" + +msgctxt "#30906" +msgid "Browse by Show in all Channels" +msgstr "Browse by Show in all Channels" + +msgctxt "#30907" +msgid "Browse Shows by Channel" +msgstr "Browse Shows by Channel" + +msgctxt "#30908" +msgid "Database Information" +msgstr "Database Information" + +# Context Menu +msgctxt "#30921" +msgid "Download Video" +msgstr "Download Video" + +msgctxt "#30922" +msgid "Download LoRes Video" +msgstr "Download LoRes Video" + +msgctxt "#30923" +msgid "Download HD Video" +msgstr "Download HD Video" + +msgctxt "#30924" +msgid "Add to queue" +msgstr "Add to queue" + +# Database status +msgctxt "#30941" +msgid "Not Initialized" +msgstr "Not Initialized" + +msgctxt "#30942" +msgid "No Status Available" +msgstr "No Status Available" + +msgctxt "#30943" +msgid "Idle" +msgstr "Idle" + +msgctxt "#30944" +msgid "Updating..." +msgstr "Updating..." + +msgctxt "#30945" +msgid "Last Update Aborted" +msgstr "Last Update Aborted" + +# Other Strings +msgctxt "#30951" +msgid "Mediathek Database Error" +msgstr "Mediathek Database Error" + +msgctxt "#30952" +msgid "Download Error" +msgstr "Download Error" + +msgctxt "#30953" +msgid "Error while downloading {}: {}" +msgstr "Error while downloading {}: {}" + +msgctxt "#30954" +msgid "Required decompression program 'xz' not found on your system" +msgstr "Required decompression program 'xz' not found on your system" + +msgctxt "#30955" +msgid "Download Database Update" +msgstr "Download Database Update" + +msgctxt "#30956" +msgid "Mediathek Database Update" +msgstr "Mediathek Database Update" + +msgctxt "#30957" +msgid "Mediathek (%d): channels:%d, shows:%d, movies:%d ..." +msgstr "Mediathek (%d): channels:%d, shows:%d, movies:%d ..." + +msgctxt "#30958" +msgid "Please set a download folder in the settings" +msgstr "Please set a download folder in the settings" + +msgctxt "#30959" +msgid "Download directory could not be created: {}" +msgstr "Download directory could not be created: {}" + +msgctxt "#30960" +msgid "Download Successful" +msgstr "Download Successful" + +msgctxt "#30961" +msgid "Welcome to MediathekView" +msgstr "Welcome to MediathekView" + +msgctxt "#30962" +msgid "This Kodi addon allows access to most video-platforms from German public service " +"broadcasters using the database provided by the popular project of MediathekView.\n\n" +"Without the endless effort of the MediathekView contributors, addons like this would not " +"exist.\n\n" +"Please consider donating to this awesome project! Visit MediathekView at https://mediathekview.de/" +msgstr "This Kodi addon allows access to most video-platforms from German public service " +"broadcasters using the database provided by the great project of MediathekView.\n\n" +"Without the endless effort of the MediathekView contributors, addons like this would not " +"exist.\n\n" +"Please consider donating to this great project! Visit MediathekView at https://mediathekview.de/" + +msgctxt "#30963" +msgid "No Decompressor Available" +msgstr "No Decompressor Available" + +msgctxt "#30964" +msgid "The database updater needs the program 'xz' in order to process the database update files. " +"This programm was not found on your Kodi system.\n\nIf you want to use the database updater on this " +"system, you have to install 'xz' in one of the standard locations (/bin, /usr/bin, /usr/local/bin).\n\n" +"Alternatively you can specify the location of the 'xz' program in the addon settings.\n\n" +"Another possibilty would be the use of an external database updated either by another Kodi system " +"or by the provided update script running on a server (see README file).\n\n" +"The datebase updater has been disabled so you have to enable it again from the addon settings page." +msgstr "The database updater needs the program 'xz' in order to process the database update files. " +"This programm was not found on your Kodi system.\n\nIf you want to use the database updater on this " +"system, you have to install 'xz' in one of the standard locations (/bin, /usr/bin, /usr/local/bin).\n\n" +"Alternatively you can specify the location of the 'xz' program in the addon settings.\n\n" +"Another possibilty would be the use of an external database updated either by another Kodi system " +"or by the provided update script running on a server (see README file).\n\n" +"The datebase updater has been disabled so you have to enable it again from the addon settings page." + +msgctxt "#30965" +msgid "Database Status: %s" +msgstr "Database Status: %s" + +msgctxt "#30966" +msgid "This database was never updated" +msgstr "This database was never updated" + +msgctxt "#30967" +msgid "%s in progress...\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies" +msgstr "%s in progress...\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies" + +msgctxt "#30968" +msgid "%s in progress...\nAdditions: %d channels, %d shows, %d movies" +msgstr "%s in progress...\nAdditions: %d channels, %d shows, %d movies" + +msgctxt "#30969" +msgid "Last %s: %s\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" +msgstr "Last %s: %s\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" + +msgctxt "#30970" +msgid "Last %s: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" +msgstr "Last %s: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" + +msgctxt "#30971" +msgid "Total: %d channels, %d shows, %d movies" +msgstr "Total: %d channels, %d shows, %d movies" + +msgctxt "#30972" +msgid "Full Update" +msgstr "Full Update" + +msgctxt "#30973" +msgid "Differential Update" +msgstr "Differential Update" + +msgctxt "#30974" +msgid "Downloading Video" +msgstr "Downloading Video" + +msgctxt "#30975" +msgid "Error while downloading {}: {}" +msgstr "Error while downloading {}: {}" + +msgctxt "#30976" +msgid "Video {} has been downloaded" +msgstr "Video {} has been downloaded" + +msgctxt "#30977" +msgid "Video Already Exists" +msgstr "Video Already Exists" + +msgctxt "#30978" +msgid "Downloading Subtitles" +msgstr "Downloading Subtitles" + +msgctxt "#30979" +msgid "Download path does not exist" +msgstr "Download path does not exist" + +msgctxt "#30980" +msgid "Results Limited" +msgstr "Results Limited" + +msgctxt "#30981" +msgid "Only the first {} results are shown." +msgstr "Only the first {} results are shown." diff --git a/plugin.video.mediathekview/resources/language/resource.language.it_it/strings.po b/plugin.video.mediathekview/resources/language/resource.language.it_it/strings.po new file mode 100644 index 0000000..f0d9027 --- /dev/null +++ b/plugin.video.mediathekview/resources/language/resource.language.it_it/strings.po @@ -0,0 +1,311 @@ +# Kodi Media Center language file +# Addon Name: MediathekView +# Addon id: plugin.video.mediathekview +# Addon Provider: leo.moll@yeasoft.com +msgid "" +msgstr "" +"Project-Id-Version: Kodi Addons\n" +"Report-Msgid-Bugs-To: leo.moll@yeasoft.com\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Alex\n" +"Language-Team: Italian (http://www.transifex.com/projects/p/xbmc-addons/language/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Settings categories +msgctxt "#30001" +msgid "General" +msgstr "Visualizzazione" + +msgctxt "#30002" +msgid "Database Settings" +msgstr "Impostazioni Banca Dati" + +# Settings Page 1 +msgctxt "#30110" +msgid "Prefer HD streams" +msgstr "Preferisci alta definizione" + +msgctxt "#30111" +msgid "No future videos (usually trailers)" +msgstr "Non mostrare video futuri (usualmente trailer)" + +msgctxt "#30112" +msgid "Minimum duration in minutes" +msgstr "Durata minima in minuti" + +msgctxt "#30113" +msgid "Group shows of different channel" +msgstr "Raggruppa trasmissioni su canali diversi" + +msgctxt "#30114" +msgid "Limit search results" +msgstr "Limita risultati ricerche" + +msgctxt "#30115" +msgid "Download directory" +msgstr "Directory di download" + +# Settings Page 2 +msgctxt "#30210" +msgid "Database type" +msgstr "Tipo banca dati" + +msgctxt "#30211" +msgid "Database hostname" +msgstr "Hostname banca dati" + +msgctxt "#30212" +msgid "Database port" +msgstr "Numero port banca dati" + +msgctxt "#30213" +msgid "Database username" +msgstr "Nome utente" + +msgctxt "#30213" +msgid "Database password" +msgstr "Password" + +msgctxt "#30214" +msgid "Database name" +msgstr "Nome Database" + +msgctxt "#30221" +msgid "Internal (sqlite)" +msgstr "Interna (sqlite)" + +msgctxt "#30222" +msgid "External (mysql)" +msgstr "Esterna (mysql)" + +msgctxt "#30231" +msgid "Database Updates" +msgstr "Attualizzazione Database" + +msgctxt "#30232" +msgid "Update interval in hours" +msgstr "Intervallo di attualizzazione in ore" + +# Main Menu +msgctxt "#30901" +msgid "Search in Title" +msgstr "Ricerca nel Titolo" + +msgctxt "#30902" +msgid "Search in Title and Description" +msgstr "Ricerca nel Titolo e nella Descrizione" + +msgctxt "#30903" +msgid "Livestreams" +msgstr "Livestreams" + +msgctxt "#30904" +msgid "Recently Added" +msgstr "Recenti" + +msgctxt "#30905" +msgid "Recently Added by Channel" +msgstr "Recenti secondo emittenti" + +msgctxt "#30906" +msgid "Browse by Show in all Channels" +msgstr "Tutte le trasmissioni" + +msgctxt "#30907" +msgid "Browse Shows by Channel" +msgstr "Trasmissioni secondo emittenti" + +msgctxt "#30908" +msgid "Database Information" +msgstr "Informazioni Database" + +# Context Menu +msgctxt "#30921" +msgid "Download Video" +msgstr "Scarica video" + +msgctxt "#30922" +msgid "Download LoRes Video" +msgstr "Scarica video SD" + +msgctxt "#30923" +msgid "Download HD Video" +msgstr "Scarica video HD" + +msgctxt "#30924" +msgid "Add to queue" +msgstr "Metti in coda" + +# Database status +msgctxt "#30941" +msgid "Not Initialized" +msgstr "Non inizializzato" + +msgctxt "#30942" +msgid "No Status Available" +msgstr "Stato non disponibile" + +msgctxt "#30943" +msgid "Idle" +msgstr "Disponibile" + +msgctxt "#30944" +msgid "Updating..." +msgstr "Attualizzazione in corso..." + +msgctxt "#30945" +msgid "Last Update Aborted" +msgstr "Ultima attualizzazione interrotta" + +# Other Strings +msgctxt "#30951" +msgid "Mediathek Database Error" +msgstr "Errore Database Mediathek" + +msgctxt "#30952" +msgid "Download Error" +msgstr "Errore di Scaricamento" + +msgctxt "#30953" +msgid "Error while downloading {}: {}" +msgstr "Errore scaricando {0}: {1}" + +msgctxt "#30954" +msgid "Required decompression program 'xz' not found on your system" +msgstr "Il decompressore 'xz' manca sul sistema" + +msgctxt "#30955" +msgid "Download Database Update" +msgstr "Attualizzazione viene scaricata" + +msgctxt "#30956" +msgid "Mediathek Database Update" +msgstr "Mediathek in attualizzazione" + +msgctxt "#30957" +msgid "Mediathek (%d): channels:%d, shows:%d, movies:%d ..." +msgstr "Mediathek (%d): canali:%d, programmi:%d, film:%d ..." + +msgctxt "#30958" +msgid "Please set a download folder in the settings" +msgstr "Selezionare una directory di download nelle impostazioni" + +msgctxt "#30959" +msgid "Download directory could not be created: {}" +msgstr "Non è stato possibile creare la directory di download: {}" + +msgctxt "#30960" +msgid "Download Successful" +msgstr "Download Completato" + +msgctxt "#30961" +msgid "Welcome to MediathekView" +msgstr "Benvenuti" + +msgctxt "#30962" +msgid "This Kodi addon allows access to most video-platforms from German public service " +"broadcasters using the database provided by the popular project of MediathekView.\n\n" +"Without the endless effort of the MediathekView contributors, addons like this would not " +"exist.\n\n" +"Please consider donating to this awesome project! Visit MediathekView at https://mediathekview.de/" +msgstr "Questo addon di Kodi permette l'accesso a gran parte delle piattaforme video operate dalle " +"emittenti pubbliche tedesche usando la banca dati del grandioso progetto MediathekView.\n\n" +"Senza l'assiduo impegno del team di MediathekView un addon come questo non sarebba mai " +"stato fattibile.\n\n" +"Per aiutare i programmatori ad offrire continui aggiornamente al database, è possibile " +"donare un piccolo contributo sulla pagina del progetto https://mediathekview.de/" + +msgctxt "#30963" +msgid "No Decompressor Available" +msgstr "Decompressore non disponibile" + +msgctxt "#30964" +msgid "The database updater needs the program 'xz' in order to process the database update files. " +"This programm was not found on your Kodi system.\n\nIf you want to use the database er on this " +"system, you have to install 'xz' in one of the standard locations (/bin, /usr/bin, /usr/local/bin).\n\n" +"Alternatively you can specify the location of the 'xz' program in the addon settings.\n\n" +"Another possibilty would be the use of an external database updated either by another Kodi system " +"or by the provided update script running on a server (see README file).\n\n" +"The datebase er has been disabled so you have to enable it again from the addon settings page." +msgstr "L'attualizzatore del database necessita il programma di decompressione 'xz' che purtroppo " +"non è stato trovato su questo sistema Kodi.\n\nSe si vuole usare l'attualizzatore del database su " +"questo sistema, è necessario installare il programma di decompressione in una delle directory " +"standard (/bin, /usr/bin, /usr/local/bin).\n\nSu sistemi Windows o nel caso il programma di " +"decompressione sia installato in una directory alternativa, il percorso potrà essere specificato " +"nelle impostazioni dell'addon.\n\n" +"In alternativa è possibile utilizzare un database esterno attualizzato da un altro sistema Kodi " +"o dallo script in allegato operato su un server unix (vedi file di README).\n\n" +"L'attualizzatore è stato disabilitato e potrà essere riattivato nelle impostazione dell'addon." + +msgctxt "#30965" +msgid "Database Status: %s" +msgstr "Stato del Database: %s" + +msgctxt "#30966" +msgid "This database was never updated" +msgstr "Questo database non è stato mai attualizzato" + +msgctxt "#30967" +msgid "%s in progress...\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies" +msgstr "%s in corso...\nAggiornamento del %s\nAggiunzioni: %d canali, %d programmi, %d film" + +msgctxt "#30968" +msgid "%s in progress...\nAdditions: %d channels, %d shows, %d movies" +msgstr "%s in corso...\nAggiunzioni: %d canali, %d programmi, %d film" + +msgctxt "#30969" +msgid "Last %s: %s\nDatabase: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" +msgstr "Ultimo %s: %s\nAggiornamento del %s\nAggiunzioni: %d canali, %d programmi, %d film\nCancellazioni: %d canali, %d programmi, %d film" + +msgctxt "#30970" +msgid "Last %s: %s\nAdditions: %d channels, %d shows, %d movies\nDeletions: %d channels, %d shows, %d movies" +msgstr "Ultimo %s: %s\nAggiunzioni: %d canali, %d programmi, %d film\nCancellazioni: %d canali, %d programmi, %d film" + +msgctxt "#30971" +msgid "Total: %d channels, %d shows, %d movies" +msgstr "Somme: %d canali, %d programmi, %d film" + +msgctxt "#30972" +msgid "Full Update" +msgstr "Aggiornamento Completo" + +msgctxt "#30973" +msgid "Differential Update" +msgstr "Aggiornamento Differenziale" + +msgctxt "#30974" +msgid "Downloading Video" +msgstr "Scaricamento video" + +msgctxt "#30975" +msgid "Error while downloading {}: {}" +msgstr "Errore durante lo scaricamento di {}: {}" + +msgctxt "#30976" +msgid "Video {} has been downloaded" +msgstr "Il video {} è stato scaricato" + +msgctxt "#30977" +msgid "Video Already Exists" +msgstr "Il video è già esistente" + +msgctxt "#30978" +msgid "Downloading Subtitles" +msgstr "Scaricamento sottotitoli" + +msgctxt "#30979" +msgid "Download path does not exist" +msgstr "Directory di scaricamento non esiste" + +msgctxt "#30980" +msgid "Results Limited" +msgstr "Risultati limitati" + +msgctxt "#30981" +msgid "Only the first {} results are shown." +msgstr "Vengono mostrati solo i primi {} risultati." diff --git a/plugin.video.mediathekview/resources/lib/__init__.py b/plugin.video.mediathekview/resources/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin.video.mediathekview/resources/lib/base/Logger.py b/plugin.video.mediathekview/resources/lib/base/Logger.py new file mode 100644 index 0000000..3d74680 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/base/Logger.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ + +# -- Classes ------------------------------------------------ +class Logger( object ): + def __init__( self, name, version, topic = None ): + self.name = name + self.version = version + self.setTopic( topic ) + + def getNewLogger( self, topic = None ): + pass + + def setTopic( self, topic = None ): + if topic == None: + self.prefix = '[%s-%s]: ' % ( self.name, self.version ) + else: + self.prefix = '[%s-%s:%s]: ' % ( self.name, self.version, topic ) + + def debug( self, message, *args ): + pass + + def info( self, message, *args ): + pass + + def warn( self, message, *args ): + pass + + def error( self, message, *args ): + pass diff --git a/plugin.video.mediathekview/resources/lib/base/__init__.py b/plugin.video.mediathekview/resources/lib/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin.video.mediathekview/resources/lib/channel.py b/plugin.video.mediathekview/resources/lib/channel.py new file mode 100644 index 0000000..d142383 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/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/resources/lib/channelui.py b/plugin.video.mediathekview/resources/lib/channelui.py new file mode 100644 index 0000000..36743e8 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/channelui.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ +import sys, urllib +import xbmcplugin, xbmcgui + +from resources.lib.channel import Channel + +# -- Classes ------------------------------------------------ +class ChannelUI( Channel ): + def __init__( self, handle, sortmethods = None, nextdir = 'initial' ): + self.base_url = sys.argv[0] + self.nextdir = nextdir + self.handle = handle + self.sortmethods = sortmethods if sortmethods is not None else [ xbmcplugin.SORT_METHOD_TITLE ] + 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.nextdir, + '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/resources/lib/exceptions.py b/plugin.video.mediathekview/resources/lib/exceptions.py new file mode 100644 index 0000000..53d7f14 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/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/resources/lib/film.py b/plugin.video.mediathekview/resources/lib/film.py new file mode 100644 index 0000000..b417078 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/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/resources/lib/filmui.py b/plugin.video.mediathekview/resources/lib/filmui.py new file mode 100644 index 0000000..af04798 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/filmui.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ +import xbmcplugin, xbmcgui + +from resources.lib.film import Film +from resources.lib.settings import Settings + +# -- Classes ------------------------------------------------ +class FilmUI( Film ): + def __init__( self, plugin, sortmethods = None ): + self.plugin = plugin + self.handle = plugin.addon_handle + self.settings = Settings() + self.sortmethods = sortmethods if sortmethods is not None else [ xbmcplugin.SORT_METHOD_TITLE, xbmcplugin.SORT_METHOD_DATE, xbmcplugin.SORT_METHOD_DURATION, xbmcplugin.SORT_METHOD_SIZE ] + 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, totalItems = 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 + if airedstring[:4] != '1970': + 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 ) + + if totalItems is not None: + xbmcplugin.addDirectoryItem( + handle = self.handle, + url = videourl, + listitem = li, + isFolder = False, + totalItems = totalItems + ) + else: + 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/resources/lib/initialui.py b/plugin.video.mediathekview/resources/lib/initialui.py new file mode 100644 index 0000000..71d9aef --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/initialui.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ +import sys, urllib +import xbmcplugin, xbmcgui + +# -- Classes ------------------------------------------------ +class InitialUI( object ): + def __init__( self, handle, sortmethods = None ): + self.handle = handle + self.sortmethods = sortmethods if sortmethods is not None else [ xbmcplugin.SORT_METHOD_TITLE ] + 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 = _build_url( { + 'mode': "shows", + 'channel': self.channelid, + 'initial': self.initial, + 'count': self.count + } ), + listitem = li, + isFolder = True + ) + + def End( self ): + xbmcplugin.endOfDirectory( self.handle ) + +# -- Functions ---------------------------------------------- + +def _build_url( query ): + return sys.argv[0] + '?' + urllib.urlencode( query ) diff --git a/plugin.video.mediathekview/resources/lib/kodi/KodiAddon.py b/plugin.video.mediathekview/resources/lib/kodi/KodiAddon.py new file mode 100644 index 0000000..f9c0cd7 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/kodi/KodiAddon.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ +import sys, urllib +import xbmc, xbmcgui, xbmcaddon, xbmcplugin + +from resources.lib.kodi.KodiLogger import KodiLogger + +# -- Classes ------------------------------------------------ +class KodiAddon( KodiLogger ): + + def __init__( self ): + self.addon = xbmcaddon.Addon() + self.addon_id = self.addon.getAddonInfo( 'id' ) + self.icon = self.addon.getAddonInfo( 'icon' ) + self.fanart = self.addon.getAddonInfo( 'fanart' ) + self.version = self.addon.getAddonInfo( 'version' ) + self.path = self.addon.getAddonInfo( 'path' ) + self.datapath = xbmc.translatePath( self.addon.getAddonInfo('profile').decode('utf-8') ) + self.language = self.addon.getLocalizedString + KodiLogger.__init__( self, self.addon_id, self.version ) + + def getSetting( self, setting_id ): + return self.addon.getSetting( setting_id ) + + def setSetting( self, setting_id, value ): + return self.addon.setSetting( setting_id, value ) + + def doAction( self, action ): + xbmc.executebuiltin( 'Action({})'.format( action ) ) + +class KodiService( KodiAddon ): + def __init__( self ): + KodiAddon.__init__( self ) + +class KodiPlugin( KodiAddon ): + def __init__( self ): + KodiAddon.__init__( self ) + self.base_url = sys.argv[0] + self.addon_handle = int( sys.argv[1] ) + + def build_url( self, query ): + return self.base_url + '?' + urllib.urlencode( query ) + + def runPlugin( self, params ): + xbmc.executebuiltin( 'RunPlugin({})'.format( self.build_url( params ) ) ) + + def addActionItem( self, name, params ): + self.addDirectoryItem( name, params, False ) + + def addFolderItem( self, name, params ): + self.addDirectoryItem( name, params, True ) + + def addDirectoryItem( self, name, params, isFolder ): + if isinstance( name, int ): + name = self.language( name ) + li = xbmcgui.ListItem( name ) + xbmcplugin.addDirectoryItem( + handle = self.addon_handle, + url = self.build_url( params ), + listitem = li, + isFolder = isFolder + ) + + def endOfDirectory( self, succeeded = True, updateListing = False, cacheToDisc = True ): + xbmcplugin.endOfDirectory( self.addon_handle, succeeded, updateListing, cacheToDisc ) diff --git a/plugin.video.mediathekview/resources/lib/kodi/KodiLogger.py b/plugin.video.mediathekview/resources/lib/kodi/KodiLogger.py new file mode 100644 index 0000000..c57ec84 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/kodi/KodiLogger.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ +import xbmc + +from resources.lib.base.Logger import Logger + +# -- Classes ------------------------------------------------ +class KodiLogger( Logger ): + + def __init__( self, name, version, topic = None ): + super( KodiLogger, self ).__init__( name, version, topic) + + def getNewLogger( self, topic = None ): + return KodiLogger( self.name, self.version, topic ) + + def debug( self, message, *args ): + self._log( xbmc.LOGDEBUG, message, *args ) + + def info( self, message, *args ): + self._log( xbmc.LOGNOTICE, message, *args ) + + def warn( self, message, *args ): + self._log( xbmc.LOGWARNING, message, *args ) + + def error( self, message, *args ): + self._log( xbmc.LOGERROR, 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 ) + xbmc.log( self.prefix + message.format( *parts ), level = level ) diff --git a/plugin.video.mediathekview/resources/lib/kodi/KodiUI.py b/plugin.video.mediathekview/resources/lib/kodi/KodiUI.py new file mode 100644 index 0000000..dee8dbb --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/kodi/KodiUI.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ +import xbmc, xbmcgui + +# -- Classes ------------------------------------------------ +class KodiUI( object ): + + def __init__( self ): + self.bgdialog = None + + def GetEnteredText( self, deftext = '', heading = '', hidden = False ): + keyboard = xbmc.Keyboard( deftext, heading, 1 if hidden else 0 ) + keyboard.doModal() + if keyboard.isConfirmed(): + return keyboard.getText() + return deftext + + def ShowNotification( self, heading, message, icon = xbmcgui.NOTIFICATION_INFO, time = 5000, sound = True ): + xbmcgui.Dialog().notification( heading, message, icon, time, sound ) + + def ShowWarning( self, heading, message, time = 5000, sound = True ): + xbmcgui.Dialog().notification( heading, message, xbmcgui.NOTIFICATION_WARNING, time, sound ) + + def ShowError( self, heading, message, time = 5000, sound = True ): + xbmcgui.Dialog().notification( heading, message, xbmcgui.NOTIFICATION_ERROR, time, sound ) + + def ShowBGDialog( self, heading = None, message = None ): + if self.bgdialog is None: + self.bgdialog = xbmcgui.DialogProgressBG() + self.bgdialog.create( heading, message ) + else: + self.bgdialog.update( 0, heading, message ) + + def UpdateBGDialog( self, percent, heading = None, message = None ): + if self.bgdialog is not None: + self.bgdialog.update( percent, heading, message ) + + def CloseBGDialog( self ): + if self.bgdialog is not None: + self.bgdialog.close() + del self.bgdialog + self.bgdialog = None + +class KodiBGDialog( object ): + def __init__( self ): + self.bgdialog= None + + def __del__( self ): + self.Close() + + def Create( self, heading = None, message = None ): + if self.bgdialog is None: + self.bgdialog = xbmcgui.DialogProgressBG() + self.bgdialog.create( heading, message ) + else: + self.bgdialog.update( 0, heading, message ) + + def Update( self, percent, heading = None, message = None ): + if self.bgdialog is not None: + self.bgdialog.update( percent, heading, message ) + + def UrlRetrieveHook( self, blockcount, blocksize, totalsize ): + downloaded = blockcount * blocksize + if totalsize > 0: + percent = int( (downloaded * 100) / totalsize ) + if self.bgdialog is not None: + self.bgdialog.update( percent ) + + def Close( self ): + if self.bgdialog is not None: + self.bgdialog.close() + del self.bgdialog + self.bgdialog = None diff --git a/plugin.video.mediathekview/resources/lib/kodi/__init__.py b/plugin.video.mediathekview/resources/lib/kodi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin.video.mediathekview/resources/lib/mvupdate.py b/plugin.video.mediathekview/resources/lib/mvupdate.py new file mode 100644 index 0000000..7595417 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/mvupdate.py @@ -0,0 +1,201 @@ +# -*- 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 resources.lib.base.Logger import Logger +from resources.lib.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.port = int( args.port ) + 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 + +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 ShowMissingExtractorError( 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: + 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 address', + default = 'localhost' + ) + mysqlopts.add_argument( + '-P', '--port', + dest = 'port', + help = 'connection port', + default = '3306' + ) + mysqlopts.add_argument( + '-u', '--user', + dest = 'user', + help = 'connection username', + default = 'mediathekview' + ) + mysqlopts.add_argument( + '-p', '--password', + dest = 'password', + help = 'connection password', + default = None + ) + mysqlopts.add_argument( + '-d', '--database', + dest = 'database', + default = 'mediathekview', + help = 'database name' + ) + 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 ) + 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/resources/lib/mvutils.py b/plugin.video.mediathekview/resources/lib/mvutils.py new file mode 100644 index 0000000..d4c7663 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/mvutils.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017-2018, Leo Moll + +# -- Imports ------------------------------------------------ +import os +import stat +import string +import urllib2 +import xbmcvfs + +from contextlib import closing + +# -- Functions ---------------------------------------------- +def dir_exists( name ): + try: + s = os.stat( name ) + return stat.S_ISDIR( s.st_mode ) + except OSError: + return False + +def file_exists( name ): + try: + s = os.stat( name ) + return stat.S_ISREG( s.st_mode ) + except OSError: + return False + +def file_size( name ): + try: + s = os.stat( name ) + return s.st_size + except OSError: + return 0 + +def find_xz(): + for xzbin in [ '/bin/xz', '/usr/bin/xz', '/usr/local/bin/xz' ]: + if file_exists( xzbin ): + return xzbin + return None + +def make_search_string( val ): + cset = string.letters + string.digits + ' _-#' + search = ''.join( [ c for c in val if c in cset ] ) + return search.upper().strip() + +def make_duration( 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 cleanup_filename( val ): + cset = string.letters + string.digits + u' _-#äöüÄÖÜßáàâéèêíìîóòôúùûÁÀÉÈÍÌÓÒÚÙçÇœ' + search = ''.join( [ c for c in val if c in cset ] ) + return search.strip() + +def url_retrieve( url, filename, reporthook, chunk_size = 8192 ): + with closing( urllib2.urlopen( url ) ) as u, closing( open( filename, 'wb' ) ) as f: + total_size = int( u.info().getheader( 'Content-Length' ).strip() ) if u.info() and u.info().getheader( 'Content-Length' ) else 0 + total_chunks = 0 + + while True: + reporthook( total_chunks, chunk_size, total_size ) + chunk = u.read( chunk_size ) + if not chunk: + break + f.write( chunk ) + total_chunks += 1 + return ( filename, [], ) + +def url_retrieve_vfs( videourl, filename, reporthook, chunk_size = 8192 ): + with closing( urllib2.urlopen( videourl ) ) as u, closing( xbmcvfs.File( filename, 'wb' ) ) as f: + total_size = int( u.info().getheader( 'Content-Length' ).strip() ) if u.info() and u.info().getheader( 'Content-Length' ) else 0 + total_chunks = 0 + + while True: + reporthook( total_chunks, chunk_size, total_size ) + chunk = u.read( chunk_size ) + if not chunk: + break + f.write( chunk ) + total_chunks += 1 + return ( filename, [], ) diff --git a/plugin.video.mediathekview/resources/lib/notifier.py b/plugin.video.mediathekview/resources/lib/notifier.py new file mode 100644 index 0000000..d257a52 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/notifier.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ +import xbmcaddon + +from resources.lib.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 ShowMissingExtractorError( self ): + self.ShowError( self.language( 30952 ), self.language( 30954 ), time = 10000 ) + + def ShowLimitResults( self, maxresults ): + self.ShowNotification( self.language( 30980 ), self.language( 30981 ).format( maxresults ) ) + + 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/resources/lib/settings.py b/plugin.video.mediathekview/resources/lib/settings.py new file mode 100644 index 0000000..32bd95a --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/settings.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ +import xbmc,xbmcaddon + +# -- Classes ------------------------------------------------ +class Settings( object ): + def __init__( self ): + self.addon = xbmcaddon.Addon() + self.Reload() + + def Reload( self ): + self.datapath = xbmc.translatePath( self.addon.getAddonInfo('profile').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.maxresults = int( self.addon.getSetting( 'maxresults' ) ) + self.downloadpath = self.addon.getSetting( 'downloadpath' ) + self.type = self.addon.getSetting( 'dbtype' ) + self.host = self.addon.getSetting( 'dbhost' ) + self.port = int( self.addon.getSetting( 'dbport' ) ) + 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 + + def HandleFirstRun( self ): + if self.firstrun: + self.firstrun = False + self.addon.setSetting( 'firstrun', 'false' ) + return True + return False diff --git a/plugin.video.mediathekview/resources/lib/show.py b/plugin.video.mediathekview/resources/lib/show.py new file mode 100644 index 0000000..e0c9127 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/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/resources/lib/showui.py b/plugin.video.mediathekview/resources/lib/showui.py new file mode 100644 index 0000000..b0bf7f9 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/showui.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll and Dominik Schlösser +# + +# -- Imports ------------------------------------------------ +import sys, urllib +import xbmcplugin, xbmcgui + +from resources.lib.show import Show + +# -- Classes ------------------------------------------------ +class ShowUI( Show ): + def __init__( self, handle, sortmethods = None ): + self.base_url = sys.argv[0] + self.handle = handle + self.sortmethods = sortmethods if sortmethods is not None else [ xbmcplugin.SORT_METHOD_TITLE ] + 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/resources/lib/store.py b/plugin.video.mediathekview/resources/lib/store.py new file mode 100644 index 0000000..2312a3c --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/store.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll +# + +# -- Imports ------------------------------------------------ +from resources.lib.storemysql import StoreMySQL +from resources.lib.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( 'StoreSQLite' ), 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 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/resources/lib/storemysql.py b/plugin.video.mediathekview/resources/lib/storemysql.py new file mode 100644 index 0000000..e2419cc --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/storemysql.py @@ -0,0 +1,913 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll +# + +# -- Imports ------------------------------------------------ +import time +import mysql.connector + +import resources.lib.mvutils as mvutils + +from resources.lib.film import Film + +# -- 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_query_filmcnt = "SELECT COUNT(*) 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, + port = self.settings.port, + user = self.settings.user, + password = self.settings.password + ) + self.conn.database = self.settings.database + except mysql.connector.Error as err: + if err.errno == mysql.connector.errorcode.ER_BAD_DB_ERROR: + self.logger.info( '=== DATABASE {} DOES NOT EXIST. TRYING TO CREATE IT ===', self.settings.database ) + self._handle_database_initialization() + return + 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, self.settings.maxresults ) + + 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, self.settings.maxresults ) + + 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, 10000 ) + + def GetLiveStreams( self, filmui ): + self._Search_Condition( '( show.search="LIVESTREAM" )', filmui, False, False, 10000 ) + + def GetChannels( self, channelui ): + self._Channels_Condition( None, channelui ) + + def GetRecentChannels( self, channelui ): + self._Channels_Condition( self.sql_cond_recent, channelui ) + + 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, 10000 ) + + 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 _Search_Condition( self, condition, filmui, showshows, showchannels, maxresults ): + 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_filmcnt + + ' WHERE ' + + condition + + self.sql_cond_nofuture + + self.sql_cond_minlength + + ' LIMIT {}'.format( maxresults + 1 ) if maxresults else '' + ) + ( results, ) = cursor.fetchone() + if maxresults and results > maxresults: + self.notifier.ShowLimitResults( maxresults ) + cursor.execute( + self.sql_query_films + + ' WHERE ' + + condition + + self.sql_cond_nofuture + + self.sql_cond_minlength + + ' LIMIT {}'.format( maxresults + 1 ) if maxresults else '' + ) + 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( totalItems = results ) + 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, mvutils.make_search_string( 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"], + mvutils.make_search_string( 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 ( idd, added ) in result: + cursor.close() + self.conn.commit() + return ( idd, 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 _handle_database_initialization( self ): + cursor = None + dbcreated = False + try: + cursor = self.conn.cursor() + cursor.execute( 'CREATE DATABASE `{}` DEFAULT CHARACTER SET utf8'.format( self.settings.database ) ) + dbcreated = True + self.conn.database = self.settings.database + cursor.execute( 'SET FOREIGN_KEY_CHECKS=0' ) + self.conn.commit() + cursor.execute( + """ +CREATE TABLE `channel` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `dtCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `touched` smallint(1) NOT NULL DEFAULT '1', + `channel` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `channel` (`channel`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + """ + ) + self.conn.commit() + + cursor.execute( """ +CREATE TABLE `film` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `dtCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `touched` smallint(1) NOT NULL DEFAULT '1', + `channelid` int(11) NOT NULL, + `showid` int(11) NOT NULL, + `title` varchar(255) NOT NULL, + `search` varchar(255) NOT NULL, + `aired` timestamp NULL DEFAULT NULL, + `duration` time DEFAULT NULL, + `size` int(11) DEFAULT NULL, + `description` longtext, + `website` varchar(384) DEFAULT NULL, + `url_sub` varchar(384) DEFAULT NULL, + `url_video` varchar(384) DEFAULT NULL, + `url_video_sd` varchar(384) DEFAULT NULL, + `url_video_hd` varchar(384) DEFAULT NULL, + `airedepoch` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `index_1` (`showid`,`title`), + KEY `index_2` (`channelid`,`title`), + KEY `dupecheck` (`channelid`,`showid`,`url_video`), + CONSTRAINT `FK_FilmChannel` FOREIGN KEY (`channelid`) REFERENCES `channel` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT `FK_FilmShow` FOREIGN KEY (`showid`) REFERENCES `show` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + """ ) + self.conn.commit() + + cursor.execute( """ +CREATE TABLE `show` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `dtCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `touched` smallint(1) NOT NULL DEFAULT '1', + `channelid` int(11) NOT NULL, + `show` varchar(255) NOT NULL, + `search` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `show` (`show`), + KEY `search` (`search`), + KEY `combined_1` (`channelid`,`search`), + KEY `combined_2` (`channelid`,`show`), + CONSTRAINT `FK_ShowChannel` FOREIGN KEY (`channelid`) REFERENCES `channel` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + """ ) + self.conn.commit() + + cursor.execute( """ +CREATE TABLE `status` ( + `modified` int(11) NOT NULL, + `status` varchar(255) NOT NULL, + `lastupdate` int(11) NOT NULL, + `filmupdate` int(11) NOT NULL, + `fullupdate` int(1) NOT NULL, + `add_chn` int(11) NOT NULL, + `add_shw` int(11) NOT NULL, + `add_mov` int(11) NOT NULL, + `del_chm` int(11) NOT NULL, + `del_shw` int(11) NOT NULL, + `del_mov` int(11) NOT NULL, + `tot_chn` int(11) NOT NULL, + `tot_shw` int(11) NOT NULL, + `tot_mov` int(11) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + """ ) + self.conn.commit() + + cursor.execute( 'INSERT INTO `status` VALUES (0,"IDLE",0,0,0,0,0,0,0,0,0,0,0,0);' ) + self.conn.commit() + + cursor.execute( 'SET FOREIGN_KEY_CHECKS=1' ) + self.conn.commit() + + cursor.execute( """ +CREATE PROCEDURE `ftInsertChannel`( + _channel VARCHAR(255) +) +BEGIN + DECLARE channelid_ INT(11); + DECLARE touched_ INT(1); + DECLARE added_ INT(1) DEFAULT 0; + + SELECT `id`, + `touched` + INTO channelid_, + touched_ + FROM `channel` + WHERE ( `channel`.`channel` = _channel ); + + IF ( channelid_ IS NULL ) THEN + INSERT INTO `channel` ( + `channel` + ) + VALUES ( + _channel + ); + SET channelid_ = LAST_INSERT_ID(); + SET added_ = 1; + ELSE + UPDATE `channel` + SET `touched` = 1 + WHERE ( `id` = channelid_ ); + END IF; + + SELECT channelid_ AS `id`, + added_ AS `added`; +END + """ ) + self.conn.commit() + + cursor.execute( """ +CREATE PROCEDURE `ftInsertFilm`( + _channelid INT(11), + _showid INT(11), + _title VARCHAR(255), + _search VARCHAR(255), + _aired TIMESTAMP, + _duration TIME, + _size INT(11), + _description LONGTEXT, + _website VARCHAR(384), + _url_sub VARCHAR(384), + _url_video VARCHAR(384), + _url_video_sd VARCHAR(384), + _url_video_hd VARCHAR(384), + _airedepoch INT(11) +) +BEGIN + DECLARE id_ INT; + DECLARE added_ INT DEFAULT 0; + + SELECT `id` + INTO id_ + FROM `film` AS f + WHERE ( f.channelid = _channelid ) + AND + ( f.showid = _showid ) + AND + ( f.url_video = _url_video ); + + IF ( id_ IS NULL ) THEN + INSERT INTO `film` ( + `channelid`, + `showid`, + `title`, + `search`, + `aired`, + `duration`, + `size`, + `description`, + `website`, + `url_sub`, + `url_video`, + `url_video_sd`, + `url_video_hd`, + `airedepoch` + ) + VALUES ( + _channelid, + _showid, + _title, + _search, + IF(_aired = "1980-01-01 00:00:00", NULL, _aired), + IF(_duration = "00:00:00", NULL, _duration), + _size, + _description, + _website, + _url_sub, + _url_video, + _url_video_sd, + _url_video_hd, + _airedepoch + ); + SET id_ = LAST_INSERT_ID(); + SET added_ = 1; + ELSE + UPDATE `film` + SET `touched` = 1 + WHERE ( `id` = id_ ); + END IF; + SELECT id_ AS `id`, + added_ AS `added`; +END + """ ) + self.conn.commit() + + cursor.execute( """ +CREATE PROCEDURE `ftInsertShow`( + _channelid INT(11), + _show VARCHAR(255), + _search VARCHAR(255) +) +BEGIN + DECLARE showid_ INT(11); + DECLARE touched_ INT(1); + DECLARE added_ INT(1) DEFAULT 0; + + SELECT `id`, + `touched` + INTO showid_, + touched_ + FROM `show` + WHERE ( `show`.`channelid` = _channelid ) + AND + ( `show`.`show` = _show ); + + IF ( showid_ IS NULL ) THEN + INSERT INTO `show` ( + `channelid`, + `show`, + `search` + ) + VALUES ( + _channelid, + _show, + _search + ); + SET showid_ = LAST_INSERT_ID(); + SET added_ = 1; + ELSE + UPDATE `show` + SET `touched` = 1 + WHERE ( `id` = showid_ ); + END IF; + + + SELECT showid_ AS `id`, + added_ AS `added`; +END + """ ) + self.conn.commit() + + cursor.execute( """ +CREATE PROCEDURE `ftUpdateEnd`( + _full INT(1) +) +BEGIN + DECLARE del_chn_ INT DEFAULT 0; + DECLARE del_shw_ INT DEFAULT 0; + DECLARE del_mov_ INT DEFAULT 0; + DECLARE cnt_chn_ INT DEFAULT 0; + DECLARE cnt_shw_ INT DEFAULT 0; + DECLARE cnt_mov_ INT DEFAULT 0; + + IF ( _full = 1 ) THEN + SELECT COUNT(*) + INTO del_chn_ + FROM `channel` + WHERE ( `touched` = 0 ); + + SELECT COUNT(*) + INTO del_shw_ + FROM `show` + WHERE ( `touched` = 0 ); + + SELECT COUNT(*) + INTO del_mov_ + FROM `film` + WHERE ( `touched` = 0 ); + + DELETE FROM `show` + WHERE ( `show`.`touched` = 0 ) + AND + ( ( SELECT SUM( `film`.`touched` ) FROM `film` WHERE `film`.`showid` = `show`.`id` ) = 0 ); + + DELETE FROM `film` + WHERE ( `touched` = 0 ); + ELSE + SET del_chn_ = 0; + SET del_shw_ = 0; + SET del_mov_ = 0; + END IF; + + SELECT del_chn_ AS `del_chn`, + del_shw_ AS `del_shw`, + del_mov_ AS `del_mov`, + cnt_chn_ AS `cnt_chn`, + cnt_shw_ AS `cnt_shw`, + cnt_mov_ AS `cnt_mov`; +END + """ ) + self.conn.commit() + + cursor.execute( """ +CREATE PROCEDURE `ftUpdateStart`( + _full INT(1) +) +BEGIN + DECLARE cnt_chn_ INT DEFAULT 0; + DECLARE cnt_shw_ INT DEFAULT 0; + DECLARE cnt_mov_ INT DEFAULT 0; + + IF ( _full = 1 ) THEN + UPDATE `channel` + SET `touched` = 0; + + UPDATE `show` + SET `touched` = 0; + + UPDATE `film` + SET `touched` = 0; + END IF; + + SELECT COUNT(*) + INTO cnt_chn_ + FROM `channel`; + + SELECT COUNT(*) + INTO cnt_shw_ + FROM `show`; + + SELECT COUNT(*) + INTO cnt_mov_ + FROM `film`; + + SELECT cnt_chn_ AS `cnt_chn`, + cnt_shw_ AS `cnt_shw`, + cnt_mov_ AS `cnt_mov`; +END + """ ) + self.conn.commit() + + cursor.close() + self.logger.info( 'Database creation successfully completed' ) + except mysql.connector.Error as err: + self.logger.error( '=== DATABASE CREATION ERROR: {} ===', err ) + self.notifier.ShowDatabaseError( err ) + try: + if dbcreated: + cursor.execute( 'DROP DATABASE `{}`'.format( self.settings.database ) ) + self.conn.commit() + if cursor is not None: + cursor.close() + del cursor + if self.conn is not None: + self.conn.close() + self.conn = None + except mysql.connector.Error as err: + # should never happen + self.conn = None diff --git a/plugin.video.mediathekview/resources/lib/storesqlite.py b/plugin.video.mediathekview/resources/lib/storesqlite.py new file mode 100644 index 0000000..601db67 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/storesqlite.py @@ -0,0 +1,753 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Leo Moll +# + +# -- Imports ------------------------------------------------ +import os, time +import sqlite3 + +import resources.lib.mvutils as mvutils + +from resources.lib.film import Film +from resources.lib.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_query_filmcnt = "SELECT COUNT(*) 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 mvutils.dir_exists( self.settings.datapath ): + os.mkdir( self.settings.datapath ) + if reset == True or not mvutils.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( 'Error while opening database: {}. trying to fully reset the Database...', err ) + 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, self.settings.maxresults ) + + 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, self.settings.maxresults ) + + 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, 10000 ) + + def GetLiveStreams( self, filmui ): + self._Search_Condition( '( show.search="LIVESTREAM" )', filmui, False, False, 10000 ) + + def GetChannels( self, channelui ): + self._Channels_Condition( None, channelui ) + + def GetRecentChannels( self, channelui ): + self._Channels_Condition( self.sql_cond_recent, channelui ) + + 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, 10000 ) + + 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 _Search_Condition( self, condition, filmui, showshows, showchannels, maxresults ): + if self.conn is None: + return + try: + maxresults = int( maxresults ) + 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_filmcnt + + ' WHERE ' + + condition + + self.sql_cond_nofuture + + self.sql_cond_minlength + + ' LIMIT {}'.format( maxresults + 1 ) if maxresults else '' + ) + ( results, ) = cursor.fetchone() + if maxresults and results > maxresults: + self.notifier.ShowLimitResults( maxresults ) + cursor.execute( + self.sql_query_films + + ' WHERE ' + + condition + + self.sql_cond_nofuture + + self.sql_cond_minlength + + ' LIMIT {}'.format( maxresults ) if maxresults else '' + ) + 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( totalItems = results ) + 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'], + mvutils.make_search_string( 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'], + mvutils.make_search_string( film['title'] ), + film['airedepoch'], + mvutils.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 _file_remove( self, name ): + if mvutils.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/resources/lib/ttml2srt.py b/plugin.video.mediathekview/resources/lib/ttml2srt.py new file mode 100644 index 0000000..7bfdc47 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/ttml2srt.py @@ -0,0 +1,220 @@ +# -*- 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 +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=None): + + 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 parent_style is not None else {} + if 'style' in elem.attrib: + style.update(styles[elem.attrib['style']]) + + if 'color' in style: + result += '' % style['color'] + + if style.get('fontstyle') == 'italic': + result += '' + + 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 += '' + + if style.get('fontstyle') == 'italic': + result += '' + + 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 isinstance( outfile, str ) or isinstance( outfile, 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/resources/lib/updater.py b/plugin.video.mediathekview/resources/lib/updater.py new file mode 100644 index 0000000..afdf840 --- /dev/null +++ b/plugin.video.mediathekview/resources/lib/updater.py @@ -0,0 +1,466 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017-2018, Leo Moll + +# -- Imports ------------------------------------------------ +import os, urllib2, subprocess, ijson, datetime, time +import xml.etree.ElementTree as etree + +import resources.lib.mvutils as mvutils + +from operator import itemgetter +#from resources.lib.utils import * +from resources.lib.store import Store +from resources.lib.exceptions import DatabaseCorrupted +from resources.lib.exceptions import DatabaseLost + +# -- Unpacker support --------------------------------------- +upd_can_bz2 = False +upd_can_gz = False + +try: + import bz2 + upd_can_bz2 = True +except ImportError: + pass + +try: + import gzip + upd_can_gz = True +except ImportError: + pass + +# -- 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 + self.use_xz = mvutils.find_xz() is not 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 IsEnabled( self ): + return self.settings.updenabled + + 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 > 10800: + # 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 ): + ( _, compfile, destfile, avgrecsize ) = self._get_update_info( full ) + if not mvutils.file_exists( destfile ): + self.logger.error( 'File {} does not exists', destfile ) + return False + # estimate number of records in update file + records = int( mvutils.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 ) + with open( destfile, 'r' ) as file: + 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 + 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... + self.logger.debug( 'Could not determine date "{}" of filmliste: {}', value.strip(), err ) + except ValueError as err: + pass + + self._update_end( full, 'IDLE' ) + self.logger.info( 'Import of {} finished', destfile ) + self.notifier.CloseUpdateProgress() + return True + except KeyboardInterrupt: + 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() + except DatabaseLost as err: + self.logger.error( '{}', err ) + self.notifier.CloseUpdateProgress() + except IOError as err: + self.logger.error( 'Error {} wile processing {}', err, destfile ) + self._update_end( full, 'ABORTED' ) + self.notifier.CloseUpdateProgress() + return False + + def GetNewestList( self, full ): + ( url, compfile, destfile, avgrecsize ) = self._get_update_info( full ) + if url is None: + self.logger.error( 'No suitable archive extractor available for this system' ) + self.notifier.ShowMissingExtractorError() + return False + + # 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.ShowDownloadError( 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( ( self._get_update_url( URL ), Prio ) ) + self.logger.info( 'Found mirror {} (Priority {})', URL, Prio ) + except AttributeError: + 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.notifier.ShowDownloadProgress() + lasturl = '' + for url in urls: + try: + lasturl = url + self.logger.info( 'Trying to download {} from {}...', os.path.basename( compfile ), url ) + self.notifier.UpdateDownloadProgress( 0, url ) + result = mvutils.url_retrieve( url, filename = compfile, reporthook = self._reporthook ) + break + except urllib2.URLError as err: + self.logger.error( 'Failure downloading {}', url ) + except Exception as err: + self.logger.error( 'Failure writng {}', url ) + if result is None: + self.logger.info( 'No file downloaded' ) + self.notifier.CloseDownloadProgress() + self.notifier.ShowDownloadError( lasturl, err ) + return False + + # decompress filmliste + if self.use_xz is True: + self.logger.info( 'Trying to decompress xz file...' ) + retval = subprocess.call( [ mvutils.find_xz(), '-d', compfile ] ) + self.logger.info( 'Return {}', retval ) + elif upd_can_bz2 is True: + self.logger.info( 'Trying to decompress bz2 file...' ) + retval = self._decompress_bz2( compfile, destfile ) + self.logger.info( 'Return {}', retval ) + elif upd_can_gz is True: + self.logger.info( 'Trying to decompress gz file...' ) + retval = self._decompress_gz( compfile, destfile ) + self.logger.info( 'Return {}', retval ) + else: + # should nebver reach + pass + + self.notifier.CloseDownloadProgress() + return retval == 0 and mvutils.file_exists( destfile ) + + def _get_update_info( self, full ): + if self.use_xz is True: + ext = 'xz' + elif upd_can_bz2 is True: + ext = 'bz2' + elif upd_can_gz is True: + ext = 'gz' + else: + return ( None, None, None, 0, ) + + if full: + return ( + FILMLISTE_AKT_URL, + os.path.join( self.settings.datapath, 'Filmliste-akt.' + ext ), + os.path.join( self.settings.datapath, 'Filmliste-akt' ), + 600, + ) + else: + return ( + FILMLISTE_DIF_URL, + os.path.join( self.settings.datapath, 'Filmliste-diff.' + ext ), + os.path.join( self.settings.datapath, 'Filmliste-diff' ), + 700, + ) + + def _get_update_url( self, url ): + if self.use_xz is True: + return url + elif upd_can_bz2 is True: + return os.path.splitext( url )[0] + '.bz2' + elif upd_can_gz is True: + return os.path.splitext( url )[0] + '.gz' + else: + # should never happen since it will not be called + return None + + def _file_remove( self, name ): + if mvutils.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 + ( _, cnt_chn, cnt_shw, cnt_mov ) = self.db.ftInsertFilm( self.film, True ) + else: + self.count = self.count + 1 + ( _, 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_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 + + def _decompress_bz2( self, sourcefile, destfile ): + blocksize = 8192 + try: + with open( destfile, 'wb' ) as df, open( sourcefile, 'rb' ) as sf: + decompressor = bz2.BZ2Decompressor() + for data in iter( lambda : sf.read( blocksize ), b'' ): + df.write( decompressor.decompress( data ) ) + except Exception as err: + self.logger.error( 'bz2 decompression failed: {}'.format( err ) ) + return -1 + return 0 + + def _decompress_gz( self, sourcefile, destfile ): + blocksize = 8192 + try: + with open( destfile, 'wb' ) as df, gzip.open( sourcefile ) as sf: + for data in iter( lambda : sf.read( blocksize ), b'' ): + df.write( data ) + except Exception as err: + self.logger.error( 'gz decompression failed: {}'.format( err ) ) + return -1 + return 0 diff --git a/plugin.video.mediathekview/resources/settings.xml b/plugin.video.mediathekview/resources/settings.xml index b50510a..a1ef62b 100644 --- a/plugin.video.mediathekview/resources/settings.xml +++ b/plugin.video.mediathekview/resources/settings.xml @@ -1,19 +1,21 @@ - - - - - - + + + + + + + - - - + + + + diff --git a/plugin.video.mediathekview/resources/sql/exportstruct.sh b/plugin.video.mediathekview/resources/sql/exportstruct.sh deleted file mode 100755 index 04c18a6..0000000 --- a/plugin.video.mediathekview/resources/sql/exportstruct.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -mysqldump -u root -p --add-drop-database --add-drop-table --events --routines --no-data --databases filmliste > filmliste-init-0.sql -sed -i'' -e 's/ AUTO_INCREMENT=[0-9]*//g' filmliste-init-0.sql diff --git a/plugin.video.mediathekview/resources/sql/filmliste-mysql-v1.sql b/plugin.video.mediathekview/resources/sql/filmliste-mysql-v1.sql deleted file mode 100644 index 341fb74..0000000 --- a/plugin.video.mediathekview/resources/sql/filmliste-mysql-v1.sql +++ /dev/null @@ -1,454 +0,0 @@ --- MySQL dump 10.13 Distrib 5.7.20, for osx10.13 (x86_64) --- --- Host: localhost Database: filmliste --- ------------------------------------------------------ --- Server version 5.7.20 - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Current Database: `filmliste` --- - -/*!40000 DROP DATABASE IF EXISTS `filmliste`*/; - -CREATE DATABASE /*!32312 IF NOT EXISTS*/ `filmliste` /*!40100 DEFAULT CHARACTER SET utf8 */; - -USE `filmliste`; - --- --- Table structure for table `channel` --- - -DROP TABLE IF EXISTS `channel`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `channel` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `dtCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `touched` smallint(1) NOT NULL DEFAULT '1', - `channel` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `channel` (`channel`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `film` --- - -DROP TABLE IF EXISTS `film`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `film` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `dtCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `touched` smallint(1) NOT NULL DEFAULT '1', - `channelid` int(11) NOT NULL, - `showid` int(11) NOT NULL, - `title` varchar(255) NOT NULL, - `search` varchar(255) NOT NULL, - `aired` timestamp NULL DEFAULT NULL, - `duration` time DEFAULT NULL, - `size` int(11) DEFAULT NULL, - `description` longtext, - `website` varchar(384) DEFAULT NULL, - `url_sub` varchar(384) DEFAULT NULL, - `url_video` varchar(384) DEFAULT NULL, - `url_video_sd` varchar(384) DEFAULT NULL, - `url_video_hd` varchar(384) DEFAULT NULL, - `airedepoch` int(11) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `index_1` (`showid`,`title`), - KEY `index_2` (`channelid`,`title`), - KEY `dupecheck` (`channelid`,`showid`,`url_video`), - CONSTRAINT `FK_FilmChannel` FOREIGN KEY (`channelid`) REFERENCES `channel` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, - CONSTRAINT `FK_FilmShow` FOREIGN KEY (`showid`) REFERENCES `show` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `show` --- - -DROP TABLE IF EXISTS `show`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `show` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `dtCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `touched` smallint(1) NOT NULL DEFAULT '1', - `channelid` int(11) NOT NULL, - `show` varchar(255) NOT NULL, - `search` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `show` (`show`), - KEY `search` (`search`), - KEY `combined_1` (`channelid`,`search`), - KEY `combined_2` (`channelid`,`show`), - CONSTRAINT `FK_ShowChannel` FOREIGN KEY (`channelid`) REFERENCES `channel` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `status` --- - -DROP TABLE IF EXISTS `status`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `status` ( - `modified` int(11) NOT NULL, - `status` varchar(255) NOT NULL, - `lastupdate` int(11) NOT NULL, - `filmupdate` int(11) NOT NULL, - `fullupdate` int(1) NOT NULL, - `add_chn` int(11) NOT NULL, - `add_shw` int(11) NOT NULL, - `add_mov` int(11) NOT NULL, - `del_chm` int(11) NOT NULL, - `del_shw` int(11) NOT NULL, - `del_mov` int(11) NOT NULL, - `tot_chn` int(11) NOT NULL, - `tot_shw` int(11) NOT NULL, - `tot_mov` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `status` --- - -LOCK TABLES `status` WRITE; -/*!40000 ALTER TABLE `status` DISABLE KEYS */; -INSERT INTO `status` VALUES (0,'IDLE',0,0,0,0,0,0,0,0,0,0,0,0); -/*!40000 ALTER TABLE `status` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Dumping routines for database 'filmliste' --- -/*!50003 DROP PROCEDURE IF EXISTS `ftInsertChannel` */; -/*!50003 SET @saved_cs_client = @@character_set_client */ ; -/*!50003 SET @saved_cs_results = @@character_set_results */ ; -/*!50003 SET @saved_col_connection = @@collation_connection */ ; -/*!50003 SET character_set_client = utf8 */ ; -/*!50003 SET character_set_results = utf8 */ ; -/*!50003 SET collation_connection = utf8_general_ci */ ; -/*!50003 SET @saved_sql_mode = @@sql_mode */ ; -/*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ; -DELIMITER ;; -CREATE DEFINER=`root`@`localhost` PROCEDURE `ftInsertChannel`( - _channel VARCHAR(255) -) -BEGIN - DECLARE channelid_ INT(11); - DECLARE touched_ INT(1); - DECLARE added_ INT(1) DEFAULT 0; - - SELECT `id`, - `touched` - INTO channelid_, - touched_ - FROM `channel` - WHERE ( `channel`.`channel` = _channel ); - - IF ( channelid_ IS NULL ) THEN - INSERT INTO `channel` ( - `channel` - ) - VALUES ( - _channel - ); - SET channelid_ = LAST_INSERT_ID(); - SET added_ = 1; - ELSE - UPDATE `channel` - SET `touched` = 1 - WHERE ( `id` = channelid_ ); - END IF; - - SELECT channelid_ AS `id`, - added_ AS `added`; -END ;; -DELIMITER ; -/*!50003 SET sql_mode = @saved_sql_mode */ ; -/*!50003 SET character_set_client = @saved_cs_client */ ; -/*!50003 SET character_set_results = @saved_cs_results */ ; -/*!50003 SET collation_connection = @saved_col_connection */ ; -/*!50003 DROP PROCEDURE IF EXISTS `ftInsertFilm` */; -/*!50003 SET @saved_cs_client = @@character_set_client */ ; -/*!50003 SET @saved_cs_results = @@character_set_results */ ; -/*!50003 SET @saved_col_connection = @@collation_connection */ ; -/*!50003 SET character_set_client = utf8 */ ; -/*!50003 SET character_set_results = utf8 */ ; -/*!50003 SET collation_connection = utf8_general_ci */ ; -/*!50003 SET @saved_sql_mode = @@sql_mode */ ; -/*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ; -DELIMITER ;; -CREATE DEFINER=`root`@`localhost` PROCEDURE `ftInsertFilm`( - _channelid INT(11), - _showid INT(11), - _title VARCHAR(255), - _search VARCHAR(255), - _aired TIMESTAMP, - _duration TIME, - _size INT(11), - _description LONGTEXT, - _website VARCHAR(384), - _url_sub VARCHAR(384), - _url_video VARCHAR(384), - _url_video_sd VARCHAR(384), - _url_video_hd VARCHAR(384), - _airedepoch INT(11) -) -BEGIN - DECLARE id_ INT; - DECLARE added_ INT DEFAULT 0; - - SELECT `id` - INTO id_ - FROM `film` AS f - WHERE ( f.channelid = _channelid ) - AND - ( f.showid = _showid ) - AND - ( f.url_video = _url_video ); - - IF ( id_ IS NULL ) THEN - INSERT INTO `film` ( - `channelid`, - `showid`, - `title`, - `search`, - `aired`, - `duration`, - `size`, - `description`, - `website`, - `url_sub`, - `url_video`, - `url_video_sd`, - `url_video_hd`, - `airedepoch` - ) - VALUES ( - _channelid, - _showid, - _title, - _search, - IF(_aired = "1980-01-01 00:00:00", NULL, _aired), - IF(_duration = "00:00:00", NULL, _duration), - _size, - _description, - _website, - _url_sub, - _url_video, - _url_video_sd, - _url_video_hd, - _airedepoch - ); - SET id_ = LAST_INSERT_ID(); - SET added_ = 1; - ELSE - UPDATE `film` - SET `touched` = 1 - WHERE ( `id` = id_ ); - END IF; - SELECT id_ AS `id`, - added_ AS `added`; -END ;; -DELIMITER ; -/*!50003 SET sql_mode = @saved_sql_mode */ ; -/*!50003 SET character_set_client = @saved_cs_client */ ; -/*!50003 SET character_set_results = @saved_cs_results */ ; -/*!50003 SET collation_connection = @saved_col_connection */ ; -/*!50003 DROP PROCEDURE IF EXISTS `ftInsertShow` */; -/*!50003 SET @saved_cs_client = @@character_set_client */ ; -/*!50003 SET @saved_cs_results = @@character_set_results */ ; -/*!50003 SET @saved_col_connection = @@collation_connection */ ; -/*!50003 SET character_set_client = utf8 */ ; -/*!50003 SET character_set_results = utf8 */ ; -/*!50003 SET collation_connection = utf8_general_ci */ ; -/*!50003 SET @saved_sql_mode = @@sql_mode */ ; -/*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ; -DELIMITER ;; -CREATE DEFINER=`root`@`localhost` PROCEDURE `ftInsertShow`( - _channelid INT(11), - _show VARCHAR(255), - _search VARCHAR(255) -) -BEGIN - DECLARE showid_ INT(11); - DECLARE touched_ INT(1); - DECLARE added_ INT(1) DEFAULT 0; - - SELECT `id`, - `touched` - INTO showid_, - touched_ - FROM `show` - WHERE ( `show`.`channelid` = _channelid ) - AND - ( `show`.`show` = _show ); - - IF ( showid_ IS NULL ) THEN - INSERT INTO `show` ( - `channelid`, - `show`, - `search` - ) - VALUES ( - _channelid, - _show, - _search - ); - SET showid_ = LAST_INSERT_ID(); - SET added_ = 1; - ELSE - UPDATE `show` - SET `touched` = 1 - WHERE ( `id` = showid_ ); - END IF; - - - SELECT showid_ AS `id`, - added_ AS `added`; -END ;; -DELIMITER ; -/*!50003 SET sql_mode = @saved_sql_mode */ ; -/*!50003 SET character_set_client = @saved_cs_client */ ; -/*!50003 SET character_set_results = @saved_cs_results */ ; -/*!50003 SET collation_connection = @saved_col_connection */ ; -/*!50003 DROP PROCEDURE IF EXISTS `ftUpdateEnd` */; -/*!50003 SET @saved_cs_client = @@character_set_client */ ; -/*!50003 SET @saved_cs_results = @@character_set_results */ ; -/*!50003 SET @saved_col_connection = @@collation_connection */ ; -/*!50003 SET character_set_client = utf8 */ ; -/*!50003 SET character_set_results = utf8 */ ; -/*!50003 SET collation_connection = utf8_general_ci */ ; -/*!50003 SET @saved_sql_mode = @@sql_mode */ ; -/*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ; -DELIMITER ;; -CREATE DEFINER=`root`@`localhost` PROCEDURE `ftUpdateEnd`( - _full INT(1) -) -BEGIN - DECLARE del_chn_ INT DEFAULT 0; - DECLARE del_shw_ INT DEFAULT 0; - DECLARE del_mov_ INT DEFAULT 0; - DECLARE cnt_chn_ INT DEFAULT 0; - DECLARE cnt_shw_ INT DEFAULT 0; - DECLARE cnt_mov_ INT DEFAULT 0; - - IF ( _full = 1 ) THEN - SELECT COUNT(*) - INTO del_chn_ - FROM `channel` - WHERE ( `touched` = 0 ); - - SELECT COUNT(*) - INTO del_shw_ - FROM `show` - WHERE ( `touched` = 0 ); - - SELECT COUNT(*) - INTO del_mov_ - FROM `film` - WHERE ( `touched` = 0 ); - - DELETE FROM `show` - WHERE ( `show`.`touched` = 0 ) - AND - ( ( SELECT SUM( `film`.`touched` ) FROM `film` WHERE `film`.`showid` = `show`.`id` ) = 0 ); - - DELETE FROM `film` - WHERE ( `touched` = 0 ); - ELSE - SET del_chn_ = 0; - SET del_shw_ = 0; - SET del_mov_ = 0; - END IF; - - SELECT del_chn_ AS `del_chn`, - del_shw_ AS `del_shw`, - del_mov_ AS `del_mov`, - cnt_chn_ AS `cnt_chn`, - cnt_shw_ AS `cnt_shw`, - cnt_mov_ AS `cnt_mov`; -END ;; -DELIMITER ; -/*!50003 SET sql_mode = @saved_sql_mode */ ; -/*!50003 SET character_set_client = @saved_cs_client */ ; -/*!50003 SET character_set_results = @saved_cs_results */ ; -/*!50003 SET collation_connection = @saved_col_connection */ ; -/*!50003 DROP PROCEDURE IF EXISTS `ftUpdateStart` */; -/*!50003 SET @saved_cs_client = @@character_set_client */ ; -/*!50003 SET @saved_cs_results = @@character_set_results */ ; -/*!50003 SET @saved_col_connection = @@collation_connection */ ; -/*!50003 SET character_set_client = utf8 */ ; -/*!50003 SET character_set_results = utf8 */ ; -/*!50003 SET collation_connection = utf8_general_ci */ ; -/*!50003 SET @saved_sql_mode = @@sql_mode */ ; -/*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ; -DELIMITER ;; -CREATE DEFINER=`root`@`localhost` PROCEDURE `ftUpdateStart`( - _full INT(1) -) -BEGIN - DECLARE cnt_chn_ INT DEFAULT 0; - DECLARE cnt_shw_ INT DEFAULT 0; - DECLARE cnt_mov_ INT DEFAULT 0; - - IF ( _full = 1 ) THEN - UPDATE `channel` - SET `touched` = 0; - - UPDATE `show` - SET `touched` = 0; - - UPDATE `film` - SET `touched` = 0; - END IF; - - SELECT COUNT(*) - INTO cnt_chn_ - FROM `channel`; - - SELECT COUNT(*) - INTO cnt_shw_ - FROM `show`; - - SELECT COUNT(*) - INTO cnt_mov_ - FROM `film`; - - SELECT cnt_chn_ AS `cnt_chn`, - cnt_shw_ AS `cnt_shw`, - cnt_mov_ AS `cnt_mov`; -END ;; -DELIMITER ; -/*!50003 SET sql_mode = @saved_sql_mode */ ; -/*!50003 SET character_set_client = @saved_cs_client */ ; -/*!50003 SET character_set_results = @saved_cs_results */ ; -/*!50003 SET collation_connection = @saved_col_connection */ ; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2018-01-03 16:26:35 diff --git a/plugin.video.mediathekview/resources/sql/filmliste-sqlite-v1.sql b/plugin.video.mediathekview/resources/sql/filmliste-sqlite-v1.sql deleted file mode 100644 index 8e7ee09..0000000 --- a/plugin.video.mediathekview/resources/sql/filmliste-sqlite-v1.sql +++ /dev/null @@ -1,104 +0,0 @@ -/* - Navicat Premium Data Transfer - - Source Server : Kodi MediathekView - Source Server Type : SQLite - Source Server Version : 3012001 - Source Database : main - - Target Server Type : SQLite - Target Server Version : 3012001 - File Encoding : utf-8 - - Date: 12/27/2017 23:56:51 PM -*/ - -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; diff --git a/plugin.video.mediathekview/service.py b/plugin.video.mediathekview/service.py index a299ee4..933ce55 100644 --- a/plugin.video.mediathekview/service.py +++ b/plugin.video.mediathekview/service.py @@ -25,15 +25,13 @@ # -- Imports ------------------------------------------------ from __future__ import unicode_literals # ,absolute_import, division -import time import xbmc -from de.yeasoft.kodi.KodiAddon import KodiService +from resources.lib.kodi.KodiAddon import KodiService -from classes.store import Store -from classes.notifier import Notifier -from classes.settings import Settings -from classes.updater import MediathekViewUpdater +from resources.lib.notifier import Notifier +from resources.lib.settings import Settings +from resources.lib.updater import MediathekViewUpdater # -- Classes ------------------------------------------------ class MediathekViewMonitor( xbmc.Monitor ): @@ -56,16 +54,10 @@ class MediathekViewService( KodiService ): self.settings = Settings() self.notifier = Notifier() self.monitor = MediathekViewMonitor( self ) - self.updater = MediathekViewUpdater( self.getNewLogger( 'MediathekViewUpdater' ), self.notifier, self.settings, self.monitor ) - - def __del__( self ): - del self.updater - del self.monitor - del self.notifier - del self.settings + self.updater = MediathekViewUpdater( self.getNewLogger( 'Updater' ), self.notifier, self.settings, self.monitor ) def Init( self ): - self.info( 'Startup' ) + self.info( 'Init' ) self.updater.Init() def Run( self ): @@ -83,11 +75,11 @@ class MediathekViewService( KodiService ): # Sleep/wait for abort for 60 seconds if self.monitor.waitForAbort( 60 ): # Abort was requested while waiting. We should exit - break - self.info( 'Exiting...' ) + break + self.info( 'Shutting down...' ) def Exit( self ): - self.info( 'Shutdown' ) + self.info( 'Exit' ) self.updater.Exit() def ReloadSettings( self ): -- cgit v1.2.3