summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeo Moll <leo.moll@dtms.de>2018-01-11 12:16:45 +0100
committerMartijn Kaijser <martijn@xbmc.org>2018-01-15 18:48:50 +0100
commite5d0f50b8d39ab7fd745b8f384ec622f6b0df5d9 (patch)
tree330896b0ec477109bcf87798feb7076a65588c45
parent7614cf25cf45728579713777796a41bed914fe05 (diff)
[plugin.video.mediathekview] 0.3.4
-rw-r--r--plugin.video.mediathekview/LICENSE.txt21
-rw-r--r--plugin.video.mediathekview/README.md439
-rw-r--r--plugin.video.mediathekview/addon.py361
-rw-r--r--plugin.video.mediathekview/addon.xml65
-rw-r--r--plugin.video.mediathekview/classes/__init__.py0
-rw-r--r--plugin.video.mediathekview/classes/channel.py11
-rw-r--r--plugin.video.mediathekview/classes/channelui.py42
-rw-r--r--plugin.video.mediathekview/classes/exceptions.py9
-rw-r--r--plugin.video.mediathekview/classes/film.py21
-rw-r--r--plugin.video.mediathekview/classes/filmui.py105
-rw-r--r--plugin.video.mediathekview/classes/initialui.py47
-rw-r--r--plugin.video.mediathekview/classes/mvupdate.py200
-rw-r--r--plugin.video.mediathekview/classes/notifier.py42
-rw-r--r--plugin.video.mediathekview/classes/settings.py37
-rw-r--r--plugin.video.mediathekview/classes/show.py13
-rw-r--r--plugin.video.mediathekview/classes/showui.py47
-rw-r--r--plugin.video.mediathekview/classes/store.py127
-rw-r--r--plugin.video.mediathekview/classes/storemysql.py534
-rw-r--r--plugin.video.mediathekview/classes/storesqlite.py766
-rw-r--r--plugin.video.mediathekview/classes/ttml2srt.py221
-rw-r--r--plugin.video.mediathekview/classes/updater.py431
-rw-r--r--plugin.video.mediathekview/de/__init__.py0
-rw-r--r--plugin.video.mediathekview/de/yeasoft/__init__.py0
-rw-r--r--plugin.video.mediathekview/de/yeasoft/base/Logger.py33
-rw-r--r--plugin.video.mediathekview/de/yeasoft/base/__init__.py0
-rw-r--r--plugin.video.mediathekview/de/yeasoft/kodi/KodiAddon.py68
-rw-r--r--plugin.video.mediathekview/de/yeasoft/kodi/KodiLogger.py55
-rw-r--r--plugin.video.mediathekview/de/yeasoft/kodi/KodiUI.py76
-rw-r--r--plugin.video.mediathekview/de/yeasoft/kodi/__init__.py0
-rwxr-xr-xplugin.video.mediathekview/mvupdate36
-rw-r--r--plugin.video.mediathekview/resources/fanart.jpgbin0 -> 148223 bytes
-rw-r--r--plugin.video.mediathekview/resources/icon.jpgbin0 -> 195418 bytes
-rw-r--r--plugin.video.mediathekview/resources/language/English/strings.po300
-rw-r--r--plugin.video.mediathekview/resources/language/German/strings.po302
-rw-r--r--plugin.video.mediathekview/resources/language/Italian/strings.po303
-rw-r--r--plugin.video.mediathekview/resources/screenshot1.pngbin0 -> 253442 bytes
-rw-r--r--plugin.video.mediathekview/resources/screenshot2.pngbin0 -> 187549 bytes
-rw-r--r--plugin.video.mediathekview/resources/screenshot3.pngbin0 -> 441825 bytes
-rw-r--r--plugin.video.mediathekview/resources/screenshot4.pngbin0 -> 294790 bytes
-rw-r--r--plugin.video.mediathekview/resources/screenshot5.pngbin0 -> 313691 bytes
-rw-r--r--plugin.video.mediathekview/resources/settings.xml23
-rwxr-xr-xplugin.video.mediathekview/resources/sql/exportstruct.sh3
-rw-r--r--plugin.video.mediathekview/resources/sql/filmliste-mysql-v1.sql454
-rw-r--r--plugin.video.mediathekview/resources/sql/filmliste-sqlite-v1.sql104
-rw-r--r--plugin.video.mediathekview/service.py103
45 files changed, 5399 insertions, 0 deletions
diff --git a/plugin.video.mediathekview/LICENSE.txt b/plugin.video.mediathekview/LICENSE.txt
new file mode 100644
index 0000000..4019d34
--- /dev/null
+++ b/plugin.video.mediathekview/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-2018, Leo Moll
+
+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. \ No newline at end of file
diff --git a/plugin.video.mediathekview/README.md b/plugin.video.mediathekview/README.md
new file mode 100644
index 0000000..b356e84
--- /dev/null
+++ b/plugin.video.mediathekview/README.md
@@ -0,0 +1,439 @@
+Kodi MediathekView.de Addon
+===========================
+
+* **English Version:** Please see below
+* **Versione Italiana:** Il testo italiano si trova più in basso
+
+[1]: https://forum.mediathekview.de/category/14/offizieller-client-kodi-add-on
+[2]: https://forum.kodi.tv/showthread.php?tid=326799
+[3]: https://github.com/mediathekview/plugin.video.mediathekview/issues
+[4]: https://github.com/mediathekview/plugin.video.mediathekview
+
+Über dieses Addon
+-----------------
+
+Und schon wieder ein Kodi-Addon für deutsche Mediatheken... Wozu das ganze?
+
+Weil der Ansatz dieses Addons ein anderer ist, als der der bereits verfügbaren
+Addons: dieses Addon benutzt die Datenbank des beliebten Projektes
+"MediathekView", welche stündlich aktualisiert wird und über 200.000 Einträge
+aus allen deutschen Mediatheken enthält. Dieser Ansatz hat einige entscheidende
+Vorteile gegenüber den anderen Addons, die in der Regel die sich stetig
+verändernden Webseiten der Mediatheken der Öffentlich Rechtlichen durchsuchen.
+
+* Hohe Geschwindigkeit beim Durchsuchen und Navigieren
+* Unabhängigkeit von allen Änderungen des Seitenlayouts der Mediatheken
+* Hohe Zuverlässigkeit
+
+
+Für Fragen und Anregungen zu diesem Addon steht das [deutschsprachige Forum][1]
+zur Verfügung. Fehlermeldungen und Vorschläge für neue Features können auch
+direkt als [GitHub Issue][3] gemeldet werden. Der Quelltext steht ebenfalls in
+[GitHub][4] zur Verfügung.
+
+
+Wichtigste Features
+-------------------
+* Hintergrundaktualisierung der Datenbank
+* Blitzschnelle Navigation
+* Herunterladen von Filmen mit automatischer Erzeugung von NFO Dateien und
+ eventueller Untertitel
+* Lokale interne Datenbank oder geteilte MySQL Datenbank
+* Benutzeroberfläche verfügbar in Deutsch, Englisch und Italienisch
+
+
+Funktionsweise
+--------------
+
+Das Addon lädt die Datenbank von MediathekView herunter und importiert diese
+entweder in einer lokalen SQLite Datenbank, oder wahlweise in einer lokalen
+oder entfernten MySQL Datenbank (zur Benutzung durch mehrere Kodi-Clients).
+Während der Laufzeit von Kodi werden in einem konfigurierbaren Intervall
+(Standard: 2 Stunden) die Differenzdateien von MediathekView heruntergeladen
+und in die Datenbank integriert. Spätestens beim nächsten Kalendertag nach
+dem letzten Update wird die Aktualisierung wieder mittels des vollständigen
+Updates von Mediathekview ausgeführt.
+
+
+Systemvoraussetzungen
+---------------------
+
+Die Systemvoraussetzungen für das Addon unterscheiden sich je nach
+Konfiguration. Nach der Installation startet das Addon im lokalen Modus:
+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.
+
+
+Alternativ-Konfigurationen
+--------------------------
+
+Ist das Kodi-System zu langsam um eine eigene Datenbank zu verwalten
+(z.B. Raspberry PI mit sehr langsamer SD-Karte) oder fehlt das Programm
+'xz', so besteht die Möglichkeit das Addon auch mit einer externen
+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.
+
+Hierfür muss lediglich die entsprechende Datenbank im MySQL Server mit
+dem SQL-Skript `resources/sql/filmliste-mysql-v1.sql` erzeugt werden.
+
+Die Verbindung zur Datenbank kann in den Addon-Einstellungen im Abschnitt
+_"Datenbank Einstellungen"_ vorgenommen werden.
+
+Ist mindestens eines der angeschlossenen Kodi-Systeme in der Lage das Update
+der Datenbank durchzuführen, so ist für das Update gesorgt. Sollte dies nicht
+der Fall sein, so besteht auch die Möglichkeit, den Update-Prozess auf einem
+anderen System (z.B. das NAS, den Datenbankserver oder eine andere Maschine)
+laufen zu lassen.
+
+
+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':
+
+* ijson
+* mysql-connector
+
+Die Installation dieser Bibliotheken erfolgt durch Eingabe folgender Befehle:
+
+````
+pip install ijson
+pip install mysql-connector==2.1.4
+````
+
+Das Aktualisierungsprogramm heisst `mvupdate` und liegt im Hauptverzeichnis
+des Addons und muss auch von dort ausgeführt werden. Aus diesem Grunde muss
+das Addon in einem Verzeichnis aus der ausführenden Maschine kopiert werden.
+
+Dies kann entweder durch Herunterladen und Entpacken der Addon-ZIP-Datei
+erfolgen oder durch Klonen des Addon-Quellcode-Repositories mittels `git`
+
+````
+git clone git@github.com:mediathekview/plugin.video.mediathekview.git
+````
+
+Durch Angabe des Parameters `-h` bzw. `-h` hinter dem Datenbanktyp, gibt
+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]
+
+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)
+ -p PASSWORD, --password PASSWORD
+ password for the MySQL server connection (default:
+ None)
+ -d DATABASE, --database DATABASE
+ MySQL database for mediathekview (default: filmliste)
+````
+
+
+English Version
+===============
+
+About this Addon
+----------------
+
+Yet another Kodi Addon for the German public service video platforms... Why?
+
+Because the approach of this addon is different from that of the already
+available addons: this addon uses the database of the popular project
+_"MediathekView"_, which is updated hourly and contains more than 200,000
+entries from all German public service video platforms. This approach has
+some significant advantages over the other add-ons that usually scan the
+ever-changing websites of the German public service video platforms:
+
+* High speed browsing and navigation
+* Independence from all changes to the page layout of the media libraries
+* High reliability
+
+If you have any questions or suggestions about this addon, please feel free
+to use the [official Kodi Addon Forum topic][2] or the [German forum topic][1].
+Errors and feature requests can also be reported directly as [GitHub Issue][3].
+The source code is available as well on [GitHub][4].
+
+
+Highlights
+----------
+* Background updating of the database
+* Amazing fast navigation and search
+* Download with subtitles and automatic NFO file generation
+* Internal standalone or shared MySQL database support
+* UI localised to German, English and Italian
+
+
+How it Works
+------------
+
+The addon downloads the database from MediathekView and imports it either into
+a local SQLite database, or alternatively into a local or remote MySQL database
+(for use by multiple Kodi clients).
+During the runtime of Kodi, only the differential update files are downloaded
+from MediathekView in a configurable interval (default: 2 hours) and integrated
+into the database. By the next calendar day after the last update at the latest,
+the update will be carried out again by importing the full MediathekView
+database.
+
+
+System Requirements
+-------------------
+
+The system requirements for the addon vary depending on the configuration.
+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.
+
+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.
+
+
+Alternate Configurations
+------------------------
+
+If the Kodi system is too slow to manage its own database (e.g. Raspberry PI
+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.
+
+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`.
+
+The connection to the database can be configured in the addon settings in
+the "Database Settings" section.
+
+If at least one of the connected Kodi systems is able to update the database,
+the data is available to all Kodi systems. If this is not the case, it is
+also possible to run the update process on a different system (e.g. the NAS,
+the database server or another machine).
+
+Standalone Database Update Process
+----------------------------------
+
+A python2 interpreter as well as the unpacker 'xz' is requirered on the
+target system in order to execute the commandline update process. Additionally
+the following python libraries are required:
+
+* ijson
+* mysql-connector
+
+The required libraries can be installed via pip:
+
+````
+pip install ijson
+pip install mysql-connector==2.1.4
+````
+
+The update program is called `mvupdate` and is located in the root directory
+of the addon and must be executed from there. The whole addon has to be copied
+to the target machine.
+
+This can be either done by downloading and unpacking the addon archive or
+by cloning the source repository with `git`
+
+````
+git clone git@github.com:mediathekview/plugin.video.mediathekview.git
+````
+
+By specifying the option `-h` itself or after the requested database type,
+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]
+
+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)
+ -p PASSWORD, --password PASSWORD
+ password for the MySQL server connection (default:
+ None)
+ -d DATABASE, --database DATABASE
+ MySQL database for mediathekview (default: filmliste)
+````
+
+
+
+Versione Italiana
+=================
+
+Un altro addon Kodi per la navigazione nelle piattaforme video operate dalle
+emittenti pubbliche tedesche... Perchè?
+
+Perché l'approccio di questo addon è diverso da quello degli altri addon
+disponibili: questo addon utilizza il database del grande progetto
+_"MediathekView"_, che viene aggiornato ogni ora e contiene oltre 200.000 voci
+da tutte le piattaforme video tedesche. Questo approccio presenta alcuni
+vantaggi significativi rispetto agli altri addon, che cercano di scansionare
+i siti delle piattaforme video in tempo reale:
+
+* Navigazione nella libreria ad alta velocità
+* Indipendenza da qualsiasi modifica al layout di pagina delle librerie multimediali
+* Alta affidabilità
+
+Se avete domande o suggerimenti riguardo quest'addon, non esitate ad utilizzare
+il [forum in lingua inglese][2] o [in lingua tedesca][1] tedesco. Errori e
+suggerimenti per nuove funzionalità possono anche essere segnalati direttamente
+come [GitHub Issue][3]. Il sorgente è disponibile in un [Repository GitHub][4].
+
+
+Highlights
+----------
+* Attualizzazione della banca dati in background
+* Navigazione e ricerca velocissima
+* Scaricamento video con generazione automatica die file NFO e scaricamento
+ sottotitoli
+* Banca dati interna o banca dati condivisa a base MySQL
+* Interfaccia disponibile in Italiano, Inglese e Tedesco
+
+
+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.
+
+
+Configurazioni alternative
+--------------------------
+
+Se il sistema Kodi è troppo lento per gestire il proprio database (ad es.
+Raspberry PI con una scheda SD molto lenta) o se manca il programma 'xz',
+è anche possibile utilizzare l'addon con un database esterno (MySQL).
+
+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.
+
+Dopodiche sarà sufficiente creare la banca dati mediante lo script SQL
+disponibile in `resources/sql/filmliste-mysql-v1.sql`.
+
+Il collegamento al database può essere effettuato nelle impostazioni
+dell'addon nella sezione "Impostazioni Banca Dati".
+
+Se almeno uno dei sistemi Kodi collegati è in grado di aggiornare il database,
+l'addon funzionerà su tutti i sistemi Kodi. In caso contrario, è anche
+possibile eseguire il processo di aggiornamento su un altro sistema (ad es. il
+NAS, il server di database o un altro sistema).
+
+
+Processo esterno di aggiornamento del database
+----------------------------------------------
+
+Per eseguire il processo esterno di aggiornamento del database, è necessario
+che sul sistema sul quale il processo viene eseguito sia istallato un
+interprete python2, il programma di decompressione 'xz' e le seguenti
+librerie python:
+
+* ijson
+* mysql-connector
+
+QUeste potranno essere istallate mediante il programma pip:
+
+````
+pip install ijson
+pip install mysql-connector==2.1.4
+````
+
+Il programma di aggiornamento si chiama `mvupdate` e si trova nella directory
+principale dell'addon e dovrà essere lanciato da questa directory. L'intero
+addon dovrà essere copiato sul sistema di destinazione.
+
+Questo sarà possibile sia scaricando l'archivio dell'addon che dovrà essere
+spacchettato in loco o mediante clonaggio dai sorgenti mediante `git`
+
+````
+git clone git@github.com:mediathekview/plugin.video.mediathekview.git
+````
+
+Specificando l'opzione `-h` a se stante o a tergo del tipo di database da
+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]
+
+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)
+ -p PASSWORD, --password PASSWORD
+ password for the MySQL server connection (default:
+ None)
+ -d DATABASE, --database DATABASE
+ MySQL database for mediathekview (default: filmliste)
+````
diff --git a/plugin.video.mediathekview/addon.py b/plugin.video.mediathekview/addon.py
new file mode 100644
index 0000000..0c10b29
--- /dev/null
+++ b/plugin.video.mediathekview/addon.py
@@ -0,0 +1,361 @@
+# -*- coding: utf-8 -*-
+#
+# MIT License
+#
+# Copyright (c) 2017-2018, Leo Moll
+#
+# 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.
+
+# -- Imports ------------------------------------------------
+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
+
+from de.yeasoft.kodi.KodiAddon import KodiPlugin
+from de.yeasoft.kodi.KodiUI import KodiBGDialog
+
+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
+
+# -- Classes ------------------------------------------------
+class MediathekView( KodiPlugin ):
+
+ def __init__( self ):
+ super( MediathekView, self ).__init__()
+ self.settings = Settings()
+ 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" } )
+ # Search all
+ self.addFolderItem( 30902, { 'mode': "searchall" } )
+ # Browse livestreams
+ self.addFolderItem( 30903, { 'mode': "livestreams" } )
+ # Browse recently added
+ self.addFolderItem( 30904, { 'mode': "recent", 'channel': 0 } )
+ # Browse recently added by channel
+ self.addFolderItem( 30905, { 'mode': "recentchannels" } )
+ # Browse by Initial->Show
+ self.addFolderItem( 30906, { 'mode': "initial", 'channel': 0 } )
+ # Browse by Channel->Initial->Shows
+ self.addFolderItem( 30907, { 'mode': "channels" } )
+ # Database Information
+ self.addActionItem( 30908, { 'mode': "action-dbinfo" } )
+ self.endOfDirectory()
+
+ def showSearch( self ):
+ searchText = self.notifier.GetEnteredText( '', self.language( 30901 ).decode( 'UTF-8' ) )
+ if len( searchText ) > 2:
+ self.db.Search( searchText, FilmUI( self ) )
+ else:
+ self.endOfDirectory( False, cacheToDisc = True )
+ # self.showMainMenu()
+
+ def showSearchAll( self ):
+ searchText = self.notifier.GetEnteredText( '', self.language( 30902 ).decode( 'UTF-8' ) )
+ if len( searchText ) > 2:
+ self.db.SearchFull( searchText, FilmUI( self ) )
+ else:
+ self.endOfDirectory( False, cacheToDisc = True )
+ # self.showMainMenu()
+
+ def showDbInfo( self ):
+ info = self.db.GetStatus()
+ heading = self.language( 30907 )
+ infostr = self.language( {
+ 'NONE': 30941,
+ 'UNINIT': 30942,
+ 'IDLE': 30943,
+ 'UPDATING': 30944,
+ 'ABORTED': 30945
+ }.get( info['status'], 30941 ) )
+ infostr = self.language( 30965 ) % infostr
+ totinfo = self.language( 30971 ) % (
+ info['tot_chn'],
+ info['tot_shw'],
+ info['tot_mov']
+ )
+ updatetype = self.language( 30972 if info['fullupdate'] > 0 else 30973 )
+ if info['status'] == 'UPDATING' and info['filmupdate'] > 0:
+ updinfo = self.language( 30967 ) % (
+ updatetype,
+ datetime.datetime.fromtimestamp( info['filmupdate'] ).strftime( '%Y-%m-%d %H:%M:%S' ),
+ info['add_chn'],
+ info['add_shw'],
+ info['add_mov']
+ )
+ elif info['status'] == 'UPDATING':
+ updinfo = self.language( 30968 ) % (
+ updatetype,
+ info['add_chn'],
+ info['add_shw'],
+ info['add_mov']
+ )
+ elif info['lastupdate'] > 0 and info['filmupdate'] > 0:
+ updinfo = self.language( 30969 ) % (
+ updatetype,
+ datetime.datetime.fromtimestamp( info['lastupdate'] ).strftime( '%Y-%m-%d %H:%M:%S' ),
+ datetime.datetime.fromtimestamp( info['filmupdate'] ).strftime( '%Y-%m-%d %H:%M:%S' ),
+ info['add_chn'],
+ info['add_shw'],
+ info['add_mov'],
+ info['del_chn'],
+ info['del_shw'],
+ info['del_mov']
+ )
+ elif info['lastupdate'] > 0:
+ updinfo = self.language( 30970 ) % (
+ updatetype,
+ datetime.datetime.fromtimestamp( info['lastupdate'] ).strftime( '%Y-%m-%d %H:%M:%S' ),
+ info['add_chn'],
+ info['add_shw'],
+ info['add_mov'],
+ info['del_chn'],
+ info['del_shw'],
+ info['del_mov']
+ )
+ else:
+ updinfo = self.language( 30966 )
+
+ xbmcgui.Dialog().textviewer(
+ heading,
+ infostr + '\n\n' +
+ totinfo + '\n\n' +
+ updinfo
+ )
+
+ def doDownloadFilm( self, filmid, quality ):
+ if self.settings.downloadpath:
+ film = self.db.RetrieveFilmInfo( filmid )
+ if film is None:
+ # film not found - should never happen
+ return
+
+ # check if the download path is reachable
+ if not xbmcvfs.exists( self.settings.downloadpath ):
+ self.notifier.ShowError( self.language( 30952 ), self.language( 30979 ) )
+ return
+
+ # get the best url
+ if quality == '0' and film.url_video_sd:
+ videourl = film.url_video_sd
+ elif quality == '2' and film.url_video_hd:
+ videourl = film.url_video_hd
+ else:
+ videourl = film.url_video
+
+ # prepare names
+ showname = self._cleanup_filename( film.show )[:64]
+ filestem = self._cleanup_filename( film.title )[:64]
+ extension = os.path.splitext( videourl )[1]
+ if not extension:
+ extension = u'.mp4'
+ if not filestem:
+ filestem = u'Film-{}'.format( film.id )
+ if not showname:
+ showname = filestem
+
+ # prepare download directory and determine episode number
+ dirname = self.settings.downloadpath + showname + '/'
+ episode = 1
+ if xbmcvfs.exists( dirname ):
+ ( dirs, epfiles, ) = xbmcvfs.listdir( dirname )
+ for epfile in epfiles:
+ match = re.search( '^.* [eE][pP]([0-9]*)\.[^/]*$', epfile )
+ if match and len( match.groups() ) > 0:
+ if episode <= int( match.group(1) ):
+ episode = int( match.group(1) ) + 1
+ else:
+ xbmcvfs.mkdir( dirname )
+
+ # prepare resulting filenames
+ fileepi = filestem + u' - EP%04d' % episode
+ movname = dirname + fileepi + extension
+ srtname = dirname + fileepi + u'.srt'
+ ttmname = dirname + fileepi + u'.ttml'
+ nfoname = dirname + fileepi + u'.nfo'
+
+ # download video
+ bgd = KodiBGDialog()
+ bgd.Create( self.language( 30974 ), fileepi + extension )
+ try:
+ bgd.Update( 0 )
+ result = self._url_retrieve( videourl, movname, bgd.UrlRetrieveHook )
+ bgd.Close()
+ if result is not None:
+ self.notifier.ShowNotification( self.language( 30960 ), self.language( 30976 ).format( videourl ) )
+ except Exception as err:
+ bgd.Close()
+ self.error( 'Failure downloading {}: {}', videourl, err )
+ self.notifier.ShowError( self.language( 30952 ), self.language( 30975 ).format( videourl, err ) )
+
+ # download subtitles
+ if film.url_sub:
+ bgd = KodiBGDialog()
+ bgd.Create( self.language( 30978 ), fileepi + u'.ttml' )
+ try:
+ bgd.Update( 0 )
+ result = self._url_retrieve( film.url_sub, ttmname, bgd.UrlRetrieveHook )
+ try:
+ ttml2srt( xbmcvfs.File( ttmname, 'r' ), xbmcvfs.File( srtname, 'w' ) )
+ except Exception as err:
+ self.info( 'Failed to convert to srt: {}', err )
+ bgd.Close()
+ except Exception as err:
+ bgd.Close()
+ self.error( 'Failure downloading {}: {}', film.url_sub, err )
+
+ # create NFO Files
+ self._make_nfo_files( film, episode, dirname, nfoname, videourl )
+ else:
+ self.notifier.ShowError( self.language( 30952 ), self.language( 30958 ) )
+
+ 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( '<tvshow>\n', 'utf-8' ) )
+ file.write( bytearray( '<id></id>\n', 'utf-8' ) )
+ file.write( bytearray( '\t<title>{}</title>\n'.format( film.show ), 'utf-8' ) )
+ file.write( bytearray( '\t<sorttitle>{}</sorttitle>\n'.format( film.show ), 'utf-8' ) )
+# file.write( bytearray( '\t<year>{}</year>\n'.format( 2018 ), 'utf-8' ) ) # XXX TODO: That might be incorrect!
+ file.write( bytearray( '\t<studio>{}</studio>\n'.format( film.channel ), 'utf-8' ) )
+ file.write( bytearray( '</tvshow>\n', 'utf-8' ) )
+ file.close()
+ except Exception as err:
+ self.error( 'Failure creating show NFO file for {}: {}', videourl, err )
+
+ try:
+ file = xbmcvfs.File( filename, 'w' )
+ file.write( bytearray( '<episodedetails>\n', 'utf-8' ) )
+ file.write( bytearray( '\t<title>{}</title>\n'.format( film.title ), 'utf-8' ) )
+ file.write( bytearray( '\t<season>1</season>\n', 'utf-8' ) )
+ file.write( bytearray( '\t<episode>{}</episode>\n'.format( episode ), 'utf-8' ) )
+ file.write( bytearray( '\t<showtitle>{}</showtitle>\n'.format( film.show ), 'utf-8' ) )
+ file.write( bytearray( '\t<plot>{}</plot>\n'.format( film.description ), 'utf-8' ) )
+ file.write( bytearray( '\t<aired>{}</aired>\n'.format( film.aired ), 'utf-8' ) )
+ if film.seconds > 60:
+ file.write( bytearray( '\t<runtime>{}</runtime>\n'.format( int( film.seconds / 60 ) ), 'utf-8' ) )
+ file.write( bytearray( '\t<studio>{}</studio\n'.format( film.channel ), 'utf-8' ) )
+ file.write( bytearray( '</episodedetails>\n', 'utf-8' ) )
+ file.close()
+ 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 )
+ if mode is None:
+ self.showMainMenu()
+ elif mode[0] == 'search':
+ self.showSearch()
+ elif mode[0] == 'searchall':
+ self.showSearchAll()
+ elif mode[0] == 'livestreams':
+ self.db.GetLiveStreams( FilmUI( self, [ xbmcplugin.SORT_METHOD_LABEL ] ) )
+ elif mode[0] == 'recent':
+ 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' ) )
+ elif mode[0] == 'channels':
+ self.db.GetChannels( ChannelUI( self.addon_handle ) )
+ elif mode[0] == 'action-dbinfo':
+ self.showDbInfo()
+ elif mode[0] == 'initial':
+ channel = self.args.get( 'channel', [0] )
+ self.db.GetInitials( channel[0], InitialUI( self.addon_handle ) )
+ elif mode[0] == 'shows':
+ channel = self.args.get( 'channel', [0] )
+ initial = self.args.get( 'initial', [None] )
+ self.db.GetShows( channel[0], initial[0], ShowUI( self.addon_handle ) )
+ elif mode[0] == 'films':
+ show = self.args.get( 'show', [0] )
+ self.db.GetFilms( show[0], FilmUI( self ) )
+ elif mode[0] == 'download':
+ filmid = self.args.get( 'id', [0] )
+ quality = self.args.get( 'quality', [1] )
+ self.doDownloadFilm( filmid[0], quality[0] )
+ elif mode[0] == 'enqueue':
+ self.doEnqueueFilm( self.args.get( 'id', [0] )[0] )
+
+ def Exit( self ):
+ self.db.Exit()
+
+# -- Main Code ----------------------------------------------
+if __name__ == '__main__':
+ addon = MediathekView()
+ addon.Init()
+ addon.Do()
+ addon.Exit()
+ del addon
diff --git a/plugin.video.mediathekview/addon.xml b/plugin.video.mediathekview/addon.xml
new file mode 100644
index 0000000..864bce7
--- /dev/null
+++ b/plugin.video.mediathekview/addon.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<addon id="plugin.video.mediathekview"
+ name="MediathekView"
+ version="0.3.4"
+ provider-name="MediathekView.de, Leo Moll">
+ <requires>
+ <import addon="xbmc.python" version="2.25.0"/>
+ <import addon="script.module.requests" version="2.12.4"/>
+ <import addon="script.module.myconnpy" version="1.1.7"/>
+ <import addon="script.module.ijson" version="2.3"/>
+ </requires>
+ <extension
+ point="xbmc.python.pluginsource"
+ library="addon.py">
+ <provides>video</provides>
+ </extension>
+ <extension
+ point="xbmc.service"
+ library="service.py"
+ start="startup" />
+ <extension point="xbmc.addon.metadata">
+ <summary lang="de">Öffentlich-Rechtliche Mediatheken</summary>
+ <summary lang="en">Public service video-platforms</summary>
+ <summary lang="it">Piattaforme video dalle emittenti pubbliche</summary>
+ <description lang="de">Ermöglicht den Zugriff auf fast alle deutschen Mediatheken der öffentlich Rechtlichen basierend auf der Datenbank von MediathekView.de</description>
+ <description lang="en">Gives access to most video-platforms from German public service broadcasters using the database of MediathekView.de</description>
+ <description lang="it">Fornisce l'accesso a gran parte delle piattaforme video operate dalle emittenti pubbliche tedesche usando la banca dati di MediathekView.de</description>
+ <news>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
+v0.3.3 (2018-01-09):
+- Auflösung kann nun beim Download ausgewählt werden
+- Kommandozeilenaktualisierer wurde implementiert
+- Fehler in der Generierung von tvshow.nfo behoben
+- Die README Datei enthält nun alle Sprachen
+v0.3.2 (2018-01-08):
+- Dateien mit Umlauten im Namen konnten nicht heruntergeladen werden
+- Herunterladen bei nicht lokalem Download-Verzeichnis erfolgt nun ohne Zwischenspeicherung
+- NFO Dateien enthalten mehr Daten
+v0.3.1 (2018-01-07):
+- Herunterladen ist nun auch in nicht lokalen (VFS) Download-Verzeichnissen möglich
+- Behebung eines sehr seltenen Fehlers der zu einem ENdlos-Update führen konnte
+v0.3.0 (2018-01-07):
+- Neue Funktion zum Herunterladen von Videos mit Untertiteln und Metadaten (NFO-Dateien)
+- Behebung eines Fehlers der das Abspeichern der Untertitel URL in der Datenbank verhinderte
+</news>
+ <platform>all</platform>
+ <language>de fr</language>
+ <license>MIT License</license>
+ <forum>https://forum.kodi.tv/showthread.php?tid=326799</forum>
+ <source>https://github.com/mediathekview/plugin.video.mediathekview</source>
+ <website>https://mediathekview.de/</website>
+ <email>info@mediathekview.de</email>
+ <assets>
+ <icon>resources/icon.jpg</icon>
+ <fanart>resources/fanart.jpg</fanart>
+ <screenshot>resources/screenshot1.png</screenshot>
+ <screenshot>resources/screenshot2.png</screenshot>
+ <screenshot>resources/screenshot3.png</screenshot>
+ <screenshot>resources/screenshot4.png</screenshot>
+ <screenshot>resources/screenshot5.png</screenshot>
+ </assets>
+ </extension>
+</addon>
diff --git a/plugin.video.mediathekview/classes/__init__.py b/plugin.video.mediathekview/classes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.mediathekview/classes/__init__.py
diff --git a/plugin.video.mediathekview/classes/channel.py b/plugin.video.mediathekview/classes/channel.py
new file mode 100644
index 0000000..d142383
--- /dev/null
+++ b/plugin.video.mediathekview/classes/channel.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+
+# -- Classes ------------------------------------------------
+class Channel( object ):
+ def __init__( self ):
+ self.id = 0
+ self.channel = '' \ No newline at end of file
diff --git a/plugin.video.mediathekview/classes/channelui.py b/plugin.video.mediathekview/classes/channelui.py
new file mode 100644
index 0000000..7061013
--- /dev/null
+++ b/plugin.video.mediathekview/classes/channelui.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import sys, urllib
+import xbmcplugin, xbmcgui
+
+from classes.channel import Channel
+from classes.settings import Settings
+
+# -- Classes ------------------------------------------------
+class ChannelUI( Channel ):
+ def __init__( self, handle, sortmethods = [ xbmcplugin.SORT_METHOD_TITLE ], next = 'initial' ):
+ self.base_url = sys.argv[0]
+ self.next = next
+ self.handle = handle
+ self.sortmethods = sortmethods
+ self.count = 0
+
+ def Begin( self ):
+ for method in self.sortmethods:
+ xbmcplugin.addSortMethod( self.handle, method )
+
+ def Add( self, altname = None ):
+ resultingname = self.channel if self.count == 0 else '%s (%d)' % ( self.channel, self.count, )
+ li = xbmcgui.ListItem( label = resultingname if altname is None else altname )
+ xbmcplugin.addDirectoryItem(
+ handle = self.handle,
+ url = self.build_url( {
+ 'mode': self.next,
+ 'channel': self.id
+ } ),
+ listitem = li,
+ isFolder = True
+ )
+
+ def End( self ):
+ xbmcplugin.endOfDirectory( self.handle )
+
+ def build_url( self, query ):
+ return self.base_url + '?' + urllib.urlencode( query )
diff --git a/plugin.video.mediathekview/classes/exceptions.py b/plugin.video.mediathekview/classes/exceptions.py
new file mode 100644
index 0000000..53d7f14
--- /dev/null
+++ b/plugin.video.mediathekview/classes/exceptions.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll
+#
+
+class DatabaseCorrupted( RuntimeError ):
+ """This exception is raised when the database throws errors during update"""
+
+class DatabaseLost( RuntimeError ):
+ """This exception is raised when the connection to the database is lost during update"""
diff --git a/plugin.video.mediathekview/classes/film.py b/plugin.video.mediathekview/classes/film.py
new file mode 100644
index 0000000..b417078
--- /dev/null
+++ b/plugin.video.mediathekview/classes/film.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+
+# -- Classes ------------------------------------------------
+class Film( object ):
+ def __init__( self ):
+ self.id = 0
+ self.title = u''
+ self.show = u''
+ self.channel = u''
+ self.description = u''
+ self.seconds = 0
+ self.size = 0
+ self.aired = u''
+ self.url_sub = u''
+ self.url_video = u''
+ self.url_video_sd = u''
+ self.url_video_hd = u''
diff --git a/plugin.video.mediathekview/classes/filmui.py b/plugin.video.mediathekview/classes/filmui.py
new file mode 100644
index 0000000..eb2cfa8
--- /dev/null
+++ b/plugin.video.mediathekview/classes/filmui.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import xbmcaddon, xbmcplugin, xbmcgui
+
+from classes.film import Film
+from classes.settings import Settings
+
+# -- Classes ------------------------------------------------
+class FilmUI( Film ):
+ def __init__( self, plugin, sortmethods = [ xbmcplugin.SORT_METHOD_TITLE, xbmcplugin.SORT_METHOD_DATE, xbmcplugin.SORT_METHOD_DURATION, xbmcplugin.SORT_METHOD_SIZE ] ):
+ self.plugin = plugin
+ self.handle = plugin.addon_handle
+ self.settings = Settings()
+ self.sortmethods = sortmethods
+ self.showshows = False
+ self.showchannels = False
+
+ def Begin( self, showshows, showchannels ):
+ self.showshows = showshows
+ self.showchannels = showchannels
+ # xbmcplugin.setContent( self.handle, 'tvshows' )
+ for method in self.sortmethods:
+ xbmcplugin.addSortMethod( self.handle, method )
+
+ def Add( self, alttitle = None ):
+ # get the best url
+ videourl = self.url_video_hd if ( self.url_video_hd != "" and self.settings.preferhd ) else self.url_video if self.url_video != "" else self.url_video_sd
+ videohds = " (HD)" if ( self.url_video_hd != "" and self.settings.preferhd ) else ""
+ # exit if no url supplied
+ if videourl == "":
+ return
+
+ if alttitle is not None:
+ resultingtitle = alttitle
+ else:
+ if self.showshows:
+ resultingtitle = self.show + ': ' + self.title
+ else:
+ resultingtitle = self.title
+ if self.showchannels:
+ resultingtitle += ' [' + self.channel + ']'
+
+ infoLabels = {
+ 'title' : resultingtitle + videohds,
+ 'sorttitle' : resultingtitle,
+ 'tvshowtitle' : self.show,
+ 'plot' : self.description
+ }
+
+ if self.size > 0:
+ infoLabels['size'] = self.size * 1024 * 1024
+
+ if self.seconds > 0:
+ infoLabels['duration'] = self.seconds
+
+ if self.aired is not None:
+ airedstring = '%s' % self.aired
+ infoLabels['date'] = airedstring[8:10] + '-' + airedstring[5:7] + '-' + airedstring[:4]
+ infoLabels['aired'] = airedstring
+ infoLabels['dateadded'] = airedstring
+
+ li = xbmcgui.ListItem( resultingtitle, self.description )
+ li.setInfo( type = 'video', infoLabels = infoLabels )
+ li.setProperty( 'IsPlayable', 'true' )
+
+ # create context menu
+ contextmenu = []
+ if self.size > 0:
+ # Download video
+ contextmenu.append( (
+ self.plugin.language( 30921 ),
+ 'RunPlugin({})'.format( self.plugin.build_url( { 'mode': "download", 'id': self.id, 'quality': 1 } ) )
+ ) )
+ if self.url_video_hd:
+ # Download SD video
+ contextmenu.append( (
+ self.plugin.language( 30923 ),
+ 'RunPlugin({})'.format( self.plugin.build_url( { 'mode': "download", 'id': self.id, 'quality': 2 } ) )
+ ) )
+ if self.url_video_sd:
+ # Download SD video
+ contextmenu.append( (
+ self.plugin.language( 30922 ),
+ 'RunPlugin({})'.format( self.plugin.build_url( { 'mode': "download", 'id': self.id, 'quality': 0 } ) )
+ ) )
+ # Add to queue
+ # TODO: Enable later
+# contextmenu.append( (
+# self.plugin.language( 30924 ),
+# 'RunPlugin({})'.format( self.plugin.build_url( { 'mode': "enqueue", 'id': self.id } ) )
+# ) )
+ li.addContextMenuItems( contextmenu )
+
+ xbmcplugin.addDirectoryItem(
+ handle = self.handle,
+ url = videourl,
+ listitem = li,
+ isFolder = False
+ )
+
+ def End( self ):
+ xbmcplugin.endOfDirectory( self.handle, cacheToDisc = False )
diff --git a/plugin.video.mediathekview/classes/initialui.py b/plugin.video.mediathekview/classes/initialui.py
new file mode 100644
index 0000000..e9593d8
--- /dev/null
+++ b/plugin.video.mediathekview/classes/initialui.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import sys, urllib
+import xbmcplugin, xbmcgui
+
+from classes.settings import Settings
+
+# -- Classes ------------------------------------------------
+class InitialUI( object ):
+ def __init__( self, handle, sortmethods = [ xbmcplugin.SORT_METHOD_TITLE ] ):
+ self.handle = handle
+ self.sortmethods = sortmethods
+ self.channelid = 0
+ self.initial = ''
+ self.count = 0
+
+ def Begin( self, channelid ):
+ self.channelid = channelid
+ for method in self.sortmethods:
+ xbmcplugin.addSortMethod( self.handle, method )
+
+ def Add( self, altname = None ):
+ if altname is None:
+ resultingname = '%s (%d)' % ( self.initial if self.initial != ' ' and self.initial != '' else ' No Title', self.count )
+ else:
+ resultingname = altname
+ li = xbmcgui.ListItem( label = resultingname )
+ xbmcplugin.addDirectoryItem(
+ handle = self.handle,
+ url = self.build_url( {
+ 'mode': "shows",
+ 'channel': self.channelid,
+ 'initial': self.initial,
+ 'count': self.count
+ } ),
+ listitem = li,
+ isFolder = True
+ )
+
+ def End( self ):
+ xbmcplugin.endOfDirectory( self.handle )
+
+ def build_url( self, query ):
+ return sys.argv[0] + '?' + urllib.urlencode( query )
diff --git a/plugin.video.mediathekview/classes/mvupdate.py b/plugin.video.mediathekview/classes/mvupdate.py
new file mode 100644
index 0000000..f314679
--- /dev/null
+++ b/plugin.video.mediathekview/classes/mvupdate.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2018, Leo Moll
+
+# -- Imports ------------------------------------------------
+import os
+import sys
+import argparse
+import datetime
+import xml.etree.ElementTree as ET
+
+from de.yeasoft.base.Logger import Logger
+from classes.store import Store
+from classes.updater import MediathekViewUpdater
+
+# -- Classes ------------------------------------------------
+class Settings( object ):
+ def __init__( self, args ):
+ self.datapath = args.path if args.dbtype == 'sqlite' else './'
+ self.type = { 'sqlite' : '0', 'mysql' : '1' }.get( args.dbtype, '0' )
+ if self.type == '1':
+ self.host = args.host
+ self.user = args.user
+ self.password = args.password
+ self.database = args.database
+ self.nofuture = True
+ self.minlength = 0
+ self.groupshows = False
+ self.updenabled = True
+ self.updinterval = 3600
+ self.updxzbin = ''
+
+class AppLogger( Logger ):
+
+ def __init__( self, name, version, topic = None, verbosity = 0 ):
+ super( AppLogger, self ).__init__( name, version, topic )
+ self.verbosity = verbosity
+
+ def getNewLogger( self, topic = None ):
+ return AppLogger( self.name, self.version, topic, self.verbosity )
+
+ def debug( self, message, *args ):
+ self._log( 2, message, *args )
+
+ def info( self, message, *args ):
+ self._log( 1, message, *args )
+
+ def warn( self, message, *args ):
+ self._log( 0, message, *args )
+
+ def error( self, message, *args ):
+ self._log( -1, message, *args )
+
+ def _log( self, level, message, *args ):
+ parts = []
+ for arg in args:
+ part = arg
+ if isinstance( arg, basestring ):
+ part = arg # arg.decode('utf-8')
+ parts.append( part )
+ output = '{} {} {}{}'.format(
+ datetime.datetime.now(),
+ { -1: 'ERROR', 0: 'WARNING', 1: 'NOTICE', 2: 'DEBUG' }.get( level, 2 ),
+ self.prefix,
+ message.format( *parts )
+ )
+
+ if level < 0:
+ # error
+ sys.stderr.write( output + '\n' )
+ sys.stderr.flush()
+ elif self.verbosity >= level:
+ # other message
+ print( output )
+
+class Notifier( object ):
+ def __init__( self ):
+ pass
+ def GetEnteredText( self, deftext = '', heading = '', hidden = False ):
+ pass
+ def ShowNotification( self, heading, message, icon = None, time = 5000, sound = True ):
+ pass
+ def ShowWarning( self, heading, message, time = 5000, sound = True ):
+ pass
+ def ShowError( self, heading, message, time = 5000, sound = True ):
+ pass
+ def ShowBGDialog( self, heading = None, message = None ):
+ pass
+ def UpdateBGDialog( self, percent, heading = None, message = None ):
+ pass
+ def CloseBGDialog( self ):
+ pass
+ def ShowDatabaseError( self, err ):
+ pass
+ def ShowDownloadError( self, name, err ):
+ pass
+ def ShowMissingXZError( self ):
+ pass
+ def ShowDownloadProgress( self ):
+ pass
+ def UpdateDownloadProgress( self, percent, message = None ):
+ pass
+ def CloseDownloadProgress( self ):
+ pass
+ def ShowUpdateProgress( self ):
+ pass
+ def UpdateUpdateProgress( self, percent, count, channels, shows, movies ):
+ pass
+ def CloseUpdateProgress( self ):
+ pass
+
+class MediathekViewMonitor( object ):
+ def abortRequested( self ):
+ return False
+
+class UpdateApp( AppLogger ):
+ def __init__( self ):
+ try:
+ self.mypath = os.path.dirname( sys.argv[0] )
+ tree = ET.parse( self.mypath + '/addon.xml' )
+ version = tree.getroot().attrib['version']
+ AppLogger.__init__( self, os.path.basename( sys.argv[0] ), version )
+ except Exception as err:
+ AppLogger.__init__( self, os.path.basename( sys.argv[0] ), '0.0' )
+
+ def Init( self ):
+ parser = argparse.ArgumentParser(
+ formatter_class = argparse.ArgumentDefaultsHelpFormatter,
+ description = 'This is the standalone database updater. It downloads the current database update from mediathekview.de and integrates it in a local database'
+ )
+ parser.add_argument(
+ '-v', '--verbose',
+ default = 0,
+ action = 'count',
+ help = 'Show progress messages'
+ )
+ subparsers = parser.add_subparsers(
+ dest = 'dbtype',
+ help = 'target database'
+ )
+ sqliteopts = subparsers.add_parser( 'sqlite', formatter_class = argparse.ArgumentDefaultsHelpFormatter )
+ sqliteopts.add_argument(
+ '-p', '--path',
+ dest = 'path',
+ help = 'alternative path for the sqlite database',
+ default = './'
+ )
+ mysqlopts = subparsers.add_parser( 'mysql', formatter_class = argparse.ArgumentDefaultsHelpFormatter )
+ mysqlopts.add_argument(
+ '-H', '--host',
+ dest = 'host',
+ help = 'hostname or ip of the MySQL server',
+ default = 'localhost'
+ )
+ mysqlopts.add_argument(
+ '-u', '--user',
+ dest = 'user',
+ help = 'username for the MySQL server connection',
+ default = 'filmliste'
+ )
+ mysqlopts.add_argument(
+ '-p', '--password',
+ dest = 'password',
+ help = 'password for the MySQL server connection',
+ default = None
+ )
+ mysqlopts.add_argument(
+ '-d', '--database',
+ dest = 'database',
+ default = 'filmliste',
+ help = 'MySQL database for mediathekview'
+ )
+ self.args = parser.parse_args()
+ self.verbosity = self.args.verbose
+
+ self.info( 'Startup' )
+ self.settings = Settings( self.args )
+ self.notifier = Notifier()
+ self.monitor = MediathekViewMonitor()
+ self.updater = MediathekViewUpdater( self.getNewLogger( 'MediathekViewUpdater' ), self.notifier, self.settings, self.monitor )
+ if self.updater.PrerequisitesMissing():
+ self.error( 'Prerequisites are missing' )
+ self.Exit()
+ exit( 1 )
+ self.updater.Init()
+
+ def Run( self ):
+ self.info( 'Starting up...' )
+ updateop = self.updater.GetCurrentUpdateOperation()
+ if updateop == 1:
+ # full update
+ self.info( 'Initiating full update...' )
+ self.updater.Update( True )
+ elif updateop == 2:
+ # differential update
+ self.info( 'Initiating differential update...' )
+ self.updater.Update( False )
+ self.info( 'Exiting...' )
+
+ def Exit( self ):
+ self.updater.Exit()
diff --git a/plugin.video.mediathekview/classes/notifier.py b/plugin.video.mediathekview/classes/notifier.py
new file mode 100644
index 0000000..f76d059
--- /dev/null
+++ b/plugin.video.mediathekview/classes/notifier.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import xbmcaddon, xbmcplugin
+
+from de.yeasoft.kodi.KodiUI import KodiUI
+
+# -- Classes ------------------------------------------------
+class Notifier( KodiUI ):
+ def __init__( self ):
+ super( Notifier, self ).__init__()
+ self.language = xbmcaddon.Addon().getLocalizedString
+
+ def ShowDatabaseError( self, err ):
+ self.ShowError( self.language( 30951 ), '{}'.format( err ) )
+
+ def ShowDownloadError( self, name, err ):
+ self.ShowError( self.language( 30952 ), self.language( 30953 ).format( name, err ) )
+
+ def ShowMissingXZError( self ):
+ self.ShowError( self.language( 30952 ), self.language( 30954 ), time = 10000 )
+
+ def ShowDownloadProgress( self ):
+ self.ShowBGDialog( self.language( 30955 ) )
+
+ def UpdateDownloadProgress( self, percent, message = None ):
+ self.UpdateBGDialog( percent, message = message )
+
+ def CloseDownloadProgress( self ):
+ self.CloseBGDialog()
+
+ def ShowUpdateProgress( self ):
+ self.ShowBGDialog( self.language( 30956 ) )
+
+ def UpdateUpdateProgress( self, percent, count, channels, shows, movies ):
+ message = self.language( 30957 ) % ( count, channels, shows, movies )
+ self.UpdateBGDialog( percent, message = message )
+
+ def CloseUpdateProgress( self ):
+ self.CloseBGDialog()
diff --git a/plugin.video.mediathekview/classes/settings.py b/plugin.video.mediathekview/classes/settings.py
new file mode 100644
index 0000000..58ea805
--- /dev/null
+++ b/plugin.video.mediathekview/classes/settings.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import os
+import xbmc,xbmcaddon
+
+# -- Classes ------------------------------------------------
+class Settings( object ):
+ def __init__( self ):
+ self.addon = xbmcaddon.Addon()
+ self.Reload()
+
+ def Reload( self ):
+ self.datapath = os.path.join( xbmc.translatePath( "special://masterprofile" ).decode('utf-8'), 'addon_data', self.addon.getAddonInfo( 'id' ).decode('utf-8') )
+ self.firstrun = self.addon.getSetting( 'firstrun' ) == 'true'
+ self.preferhd = self.addon.getSetting( 'quality' ) == 'true'
+ self.nofuture = self.addon.getSetting( 'nofuture' ) == 'true'
+ self.minlength = int( float( self.addon.getSetting( 'minlength' ) ) ) * 60
+ self.groupshows = self.addon.getSetting( 'groupshows' ) == 'true'
+ self.downloadpath = self.addon.getSetting( 'downloadpath' )
+ self.type = self.addon.getSetting( 'dbtype' )
+ self.host = self.addon.getSetting( 'dbhost' )
+ self.user = self.addon.getSetting( 'dbuser' )
+ self.password = self.addon.getSetting( 'dbpass' )
+ self.database = self.addon.getSetting( 'dbdata' )
+ self.updenabled = self.addon.getSetting( 'updenabled' ) == 'true'
+ self.updinterval = int( float( self.addon.getSetting( 'updinterval' ) ) ) * 3600
+ self.updxzbin = self.addon.getSetting( 'updxzbin' )
+
+ def HandleFirstRun( self ):
+ if self.firstrun:
+ self.firstrun = False
+ self.addon.setSetting( 'firstrun', 'false' )
+ return True
+ return False
diff --git a/plugin.video.mediathekview/classes/show.py b/plugin.video.mediathekview/classes/show.py
new file mode 100644
index 0000000..e0c9127
--- /dev/null
+++ b/plugin.video.mediathekview/classes/show.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+
+# -- Classes ------------------------------------------------
+class Show( object ):
+ def __init__( self ):
+ self.id = 0
+ self.channelid = 0
+ self.show = ''
+ self.channel = '' \ No newline at end of file
diff --git a/plugin.video.mediathekview/classes/showui.py b/plugin.video.mediathekview/classes/showui.py
new file mode 100644
index 0000000..b6f0bf8
--- /dev/null
+++ b/plugin.video.mediathekview/classes/showui.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll and Dominik Schlösser
+#
+
+# -- Imports ------------------------------------------------
+import sys, urllib
+import xbmcplugin, xbmcgui
+
+from classes.show import Show
+from classes.settings import Settings
+
+# -- Classes ------------------------------------------------
+class ShowUI( Show ):
+ def __init__( self, handle, sortmethods = [ xbmcplugin.SORT_METHOD_TITLE ] ):
+ self.base_url = sys.argv[0]
+ self.handle = handle
+ self.sortmethods = sortmethods
+ self.querychannelid = 0
+
+ def Begin( self, channelid ):
+ self.querychannelid = channelid
+ for method in self.sortmethods:
+ xbmcplugin.addSortMethod( self.handle, method )
+
+ def Add( self, altname = None ):
+ if altname is not None:
+ resultingname = altname
+ elif self.querychannelid == '0':
+ resultingname = self.show + ' [' + self.channel + ']'
+ else:
+ resultingname = self.show
+ li = xbmcgui.ListItem( label = resultingname )
+ xbmcplugin.addDirectoryItem(
+ handle = self.handle,
+ url = self.build_url( {
+ 'mode': "films",
+ 'show': self.id
+ } ),
+ listitem = li,
+ isFolder = True
+ )
+
+ def End( self ):
+ xbmcplugin.endOfDirectory( self.handle )
+
+ def build_url( self, query ):
+ return self.base_url + '?' + urllib.urlencode( query )
diff --git a/plugin.video.mediathekview/classes/store.py b/plugin.video.mediathekview/classes/store.py
new file mode 100644
index 0000000..15c6ef2
--- /dev/null
+++ b/plugin.video.mediathekview/classes/store.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll
+#
+
+# -- Imports ------------------------------------------------
+from classes.storemysql import StoreMySQL
+from classes.storesqlite import StoreSQLite
+
+# -- Classes ------------------------------------------------
+class Store( object ):
+ def __init__( self, logger, notifier, settings ):
+ self.logger = logger
+ self.notifier = notifier
+ self.settings = settings
+ # load storage engine
+ if settings.type == '0':
+ self.logger.info( 'Database driver: Internal (sqlite)' )
+ self.db = StoreSQLite( logger.getNewLogger( 'StoreMySQL' ), notifier, self.settings )
+ elif settings.type == '1':
+ self.logger.info( 'Database driver: External (mysql)' )
+ self.db = StoreMySQL( logger.getNewLogger( 'StoreMySQL' ), notifier, self.settings )
+ else:
+ self.logger.warn( 'Unknown Database driver selected' )
+ self.db = None
+
+ def __del__( self ):
+ if self.db is not None:
+ del self.db
+ self.db = None
+
+ def Init( self, reset = False ):
+ if self.db is not None:
+ self.db.Init( reset )
+
+ def Exit( self ):
+ if self.db is not None:
+ self.db.Exit()
+
+ def Search( self, search, filmui ):
+ if self.db is not None:
+ self.db.Search( search, filmui )
+
+ def SearchFull( self, search, filmui ):
+ if self.db is not None:
+ self.db.SearchFull( search, filmui )
+
+ def GetRecents( self, channelid, filmui ):
+ if self.db is not None:
+ self.db.GetRecents( channelid, filmui )
+
+ def GetLiveStreams( self, filmui ):
+ if self.db is not None:
+ self.db.GetLiveStreams( filmui )
+
+ def GetChannels( self, channelui ):
+ if self.db is not None:
+ self.db.GetChannels( channelui )
+
+ def GetRecentChannels( self, channelui ):
+ if self.db is not None:
+ self.db.GetRecentChannels( channelui )
+
+ def GetInitials( self, channelid, initialui ):
+ if self.db is not None:
+ self.db.GetInitials( channelid, initialui )
+
+ def GetShows( self, channelid, initial, showui ):
+ if self.db is not None:
+ self.db.GetShows( channelid, initial, showui )
+
+ def GetFilms( self, showid, filmui ):
+ if self.db is not None:
+ self.db.GetFilms( showid, filmui )
+
+ def RetrieveFilmInfo( self, filmid ):
+ if self.db is not None:
+ return self.db.RetrieveFilmInfo( filmid )
+
+ def GetStatus( self ):
+ if self.db is not None:
+ return self.db.GetStatus()
+ else:
+ return {
+ 'modified': int( time.time() ),
+ 'status': 'UNINIT',
+ 'lastupdate': 0,
+ 'filmupdate': 0,
+ 'fullupdate': 0,
+ 'add_chn': 0,
+ 'add_shw': 0,
+ 'add_mov': 0,
+ 'del_chn': 0,
+ 'del_shw': 0,
+ 'del_mov': 0,
+ 'tot_chn': 0,
+ 'tot_shw': 0,
+ 'tot_mov': 0
+ }
+
+ def UpdateStatus( self, status = None, lastupdate = None, filmupdate = None, fullupdate = None, add_chn = None, add_shw = None, add_mov = None, del_chn = None, del_shw = None, del_mov = None, tot_chn = None, tot_shw = None, tot_mov = None ):
+ if self.db is not None:
+ self.db.UpdateStatus( status, lastupdate, filmupdate, fullupdate, add_chn, add_shw, add_mov, del_chn, del_shw, del_mov, tot_chn, tot_shw, tot_mov )
+
+ def SupportsUpdate( self ):
+ if self.db is not None:
+ return self.db.SupportsUpdate()
+ return False
+
+ def ftInit( self ):
+ if self.db is not None:
+ return self.db.ftInit()
+ return False
+
+ def ftUpdateStart( self, full ):
+ if self.db is not None:
+ return self.db.ftUpdateStart( full )
+ return ( 0, 0, 0, )
+
+ def ftUpdateEnd( self, delete ):
+ if self.db is not None:
+ return self.db.ftUpdateEnd( delete )
+ return ( 0, 0, 0, 0, 0, 0, )
+
+ def ftInsertFilm( self, film, commit = True ):
+ if self.db is not None:
+ return self.db.ftInsertFilm( film, commit )
+ return ( 0, 0, 0, 0, )
diff --git a/plugin.video.mediathekview/classes/storemysql.py b/plugin.video.mediathekview/classes/storemysql.py
new file mode 100644
index 0000000..a272277
--- /dev/null
+++ b/plugin.video.mediathekview/classes/storemysql.py
@@ -0,0 +1,534 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll
+#
+
+# -- Imports ------------------------------------------------
+import string, time
+import mysql.connector
+
+from classes.film import Film
+from classes.exceptions import DatabaseCorrupted
+
+# -- Classes ------------------------------------------------
+class StoreMySQL( object ):
+ def __init__( self, logger, notifier, settings ):
+ self.conn = None
+ self.logger = logger
+ self.notifier = notifier
+ self.settings = settings
+ # useful query fragments
+ self.sql_query_films = "SELECT film.id,`title`,`show`,`channel`,`description`,TIME_TO_SEC(`duration`) AS `seconds`,`size`,`aired`,`url_sub`,`url_video`,`url_video_sd`,`url_video_hd` FROM `film` LEFT JOIN `show` ON show.id=film.showid LEFT JOIN `channel` ON channel.id=film.channelid"
+ self.sql_cond_recent = "( TIMESTAMPDIFF(HOUR,`aired`,CURRENT_TIMESTAMP()) < 24 )"
+ self.sql_cond_nofuture = " AND ( ( `aired` IS NULL ) OR ( TIMESTAMPDIFF(HOUR,`aired`,CURRENT_TIMESTAMP()) > 0 ) )" if settings.nofuture else ""
+ self.sql_cond_minlength = " AND ( ( `duration` IS NULL ) OR ( TIME_TO_SEC(`duration`) >= %d ) )" % settings.minlength if settings.minlength > 0 else ""
+
+ def Init( self, reset = False ):
+ self.logger.info( 'Using MySQL connector version {}', mysql.connector.__version__ )
+ try:
+ self.conn = mysql.connector.connect(
+ host = self.settings.host,
+ user = self.settings.user,
+ password = self.settings.password,
+ database = self.settings.database
+ )
+ except mysql.connector.Error as err:
+ self.conn = None
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def Exit( self ):
+ if self.conn is not None:
+ self.conn.close()
+
+ def Search( self, search, filmui ):
+ self._Search_Condition( '( ( `title` LIKE "%%%s%%" ) OR ( `show` LIKE "%%%s%%" ) )' % ( search, search, ), filmui, True, True )
+
+ def SearchFull( self, search, filmui ):
+ self._Search_Condition( '( ( `title` LIKE "%%%s%%" ) OR ( `show` LIKE "%%%s%%" ) ) OR ( `description` LIKE "%%%s%%") )' % ( search, search, search ), filmui, True, True )
+
+ def GetRecents( self, channelid, filmui ):
+ sql_cond_channel = ' AND ( film.channelid=' + str( channelid ) + ' ) ' if channelid != '0' else ''
+ self._Search_Condition( self.sql_cond_recent + sql_cond_channel, filmui, True, False )
+
+ def GetLiveStreams( self, filmui ):
+ self._Search_Condition( '( show.search="LIVESTREAM" )', filmui, False, False )
+
+ def GetChannels( self, channelui ):
+ self._Channels_Condition( None, channelui )
+
+ def GetRecentChannels( self, channelui ):
+ self._Channels_Condition( self.sql_cond_recent, channelui )
+
+ def _Channels_Condition( self, condition, channelui):
+ if self.conn is None:
+ return
+ try:
+ if condition is None:
+ query = 'SELECT `id`,`channel`,0 AS `count` FROM `channel`'
+ else:
+ query = 'SELECT channel.id AS `id`,`channel`,COUNT(*) AS `count` FROM `film` LEFT JOIN `channel` ON channel.id=film.channelid WHERE ' + condition + ' GROUP BY channel.id'
+ self.logger.info( 'MySQL Query: {}', query )
+ cursor = self.conn.cursor()
+ cursor.execute( query )
+ channelui.Begin()
+ for ( channelui.id, channelui.channel, channelui.count ) in cursor:
+ channelui.Add()
+ channelui.End()
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetInitials( self, channelid, initialui ):
+ if self.conn is None:
+ return
+ try:
+ condition = 'WHERE ( `channelid`=' + str( channelid ) + ' ) ' if channelid != '0' else ''
+ self.logger.info( 'MySQL Query: {}',
+ 'SELECT LEFT(`search`,1) AS letter,COUNT(*) AS `count` FROM `show` ' +
+ condition +
+ 'GROUP BY LEFT(search,1)'
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ 'SELECT LEFT(`search`,1) AS letter,COUNT(*) AS `count` FROM `show` ' +
+ condition +
+ 'GROUP BY LEFT(`search`,1)'
+ )
+ initialui.Begin( channelid )
+ for ( initialui.initial, initialui.count ) in cursor:
+ initialui.Add()
+ initialui.End()
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetShows( self, channelid, initial, showui ):
+ if self.conn is None:
+ return
+ try:
+ if channelid == '0' and self.settings.groupshows:
+ query = 'SELECT GROUP_CONCAT(show.id),GROUP_CONCAT(`channelid`),`show`,GROUP_CONCAT(`channel`) FROM `show` LEFT JOIN `channel` ON channel.id=show.channelid WHERE ( `show` LIKE "%s%%" ) GROUP BY `show`' % initial
+ elif channelid == '0':
+ query = 'SELECT show.id,show.channelid,show.show,channel.channel FROM `show` LEFT JOIN channel ON channel.id=show.channelid WHERE ( `show` LIKE "%s%%" )' % initial
+ else:
+ query = 'SELECT show.id,show.channelid,show.show,channel.channel FROM `show` LEFT JOIN channel ON channel.id=show.channelid WHERE ( `channelid`=%s ) AND ( `show` LIKE "%s%%" )' % ( channelid, initial )
+ self.logger.info( 'MySQL Query: {}', query )
+ cursor = self.conn.cursor()
+ cursor.execute( query )
+ showui.Begin( channelid )
+ for ( showui.id, showui.channelid, showui.show, showui.channel ) in cursor:
+ showui.Add()
+ showui.End()
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetFilms( self, showid, filmui ):
+ if self.conn is None:
+ return
+ if showid.find( ',' ) == -1:
+ # only one channel id
+ condition = '( `showid=`%s )' % showid
+ showchannels = False
+ else:
+ # multiple channel ids
+ condition = '( `showid` IN ( %s ) )' % showid
+ showchannels = True
+ self._Search_Condition( condition, filmui, False, showchannels )
+
+ def _Search_Condition( self, condition, filmui, showshows, showchannels ):
+ if self.conn is None:
+ return
+ try:
+ self.logger.info( 'MySQL Query: {}',
+ self.sql_query_films +
+ ' WHERE ' +
+ condition +
+ self.sql_cond_nofuture +
+ self.sql_cond_minlength
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ self.sql_query_films +
+ ' WHERE ' +
+ condition +
+ self.sql_cond_nofuture +
+ self.sql_cond_minlength
+ )
+ filmui.Begin( showshows, showchannels )
+ for ( filmui.id, filmui.title, filmui.show, filmui.channel, filmui.description, filmui.seconds, filmui.size, filmui.aired, filmui.url_sub, filmui.url_video, filmui.url_video_sd, filmui.url_video_hd ) in cursor:
+ filmui.Add()
+ filmui.End()
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def RetrieveFilmInfo( self, filmid ):
+ if self.conn is None:
+ return None
+ try:
+ condition = '( film.id={} )'.format( filmid )
+ self.logger.info( 'MySQL Query: {}',
+ self.sql_query_films +
+ ' WHERE ' +
+ condition
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ self.sql_query_films +
+ ' WHERE ' +
+ condition
+ )
+ film = Film()
+ for ( film.id, film.title, film.show, film.channel, film.description, film.seconds, film.size, film.aired, film.url_sub, film.url_video, film.url_video_sd, film.url_video_hd ) in cursor:
+ cursor.close()
+ return film
+ cursor.close()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return None
+
+ def GetStatus( self ):
+ status = {
+ 'modified': int( time.time() ),
+ 'status': '',
+ 'lastupdate': 0,
+ 'filmupdate': 0,
+ 'fullupdate': 0,
+ 'add_chn': 0,
+ 'add_shw': 0,
+ 'add_mov': 0,
+ 'del_chn': 0,
+ 'del_shw': 0,
+ 'del_mov': 0,
+ 'tot_chn': 0,
+ 'tot_shw': 0,
+ 'tot_mov': 0
+ }
+ if self.conn is None:
+ status['status'] = "UNINIT"
+ return status
+ try:
+ cursor = self.conn.cursor()
+ cursor.execute( 'SELECT * FROM `status` LIMIT 1' )
+ r = cursor.fetchall()
+ cursor.close()
+ self.conn.commit()
+ if len( r ) == 0:
+ status['status'] = "NONE"
+ return status
+ status['modified'] = r[0][0]
+ status['status'] = r[0][1]
+ status['lastupdate'] = r[0][2]
+ status['filmupdate'] = r[0][3]
+ status['fullupdate'] = r[0][4]
+ status['add_chn'] = r[0][5]
+ status['add_shw'] = r[0][6]
+ status['add_mov'] = r[0][7]
+ status['del_chn'] = r[0][8]
+ status['del_shw'] = r[0][9]
+ status['del_mov'] = r[0][10]
+ status['tot_chn'] = r[0][11]
+ status['tot_shw'] = r[0][12]
+ status['tot_mov'] = r[0][13]
+ return status
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ status['status'] = "UNINIT"
+ return status
+
+ def UpdateStatus( self, status = None, lastupdate = None, filmupdate = None, fullupdate = None, add_chn = None, add_shw = None, add_mov = None, del_chn = None, del_shw = None, del_mov = None, tot_chn = None, tot_shw = None, tot_mov = None ):
+ if self.conn is None:
+ return
+ new = self.GetStatus()
+ old = new['status']
+ if status is not None:
+ new['status'] = status
+ if lastupdate is not None:
+ new['lastupdate'] = lastupdate
+ if filmupdate is not None:
+ new['filmupdate'] = filmupdate
+ if fullupdate is not None:
+ new['fullupdate'] = fullupdate
+ if add_chn is not None:
+ new['add_chn'] = add_chn
+ if add_shw is not None:
+ new['add_shw'] = add_shw
+ if add_mov is not None:
+ new['add_mov'] = add_mov
+ if del_chn is not None:
+ new['del_chn'] = del_chn
+ if del_shw is not None:
+ new['del_shw'] = del_shw
+ if del_mov is not None:
+ new['del_mov'] = del_mov
+ if tot_chn is not None:
+ new['tot_chn'] = tot_chn
+ if tot_shw is not None:
+ new['tot_shw'] = tot_shw
+ if tot_mov is not None:
+ new['tot_mov'] = tot_mov
+ # TODO: we should only write, if we have changed something...
+ new['modified'] = int( time.time() )
+ try:
+ cursor = self.conn.cursor()
+ if old == "NONE":
+ # insert status
+ cursor.execute(
+ """
+ INSERT INTO `status` (
+ `modified`,
+ `status`,
+ `lastupdate`,
+ `filmupdate`,
+ `fullupdate`,
+ `add_chn`,
+ `add_shw`,
+ `add_mov`,
+ `del_chm`,
+ `del_shw`,
+ `del_mov`,
+ `tot_chn`,
+ `tot_shw`,
+ `tot_mov`
+ )
+ VALUES (
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s
+ )
+ """, (
+ new['modified'],
+ new['status'],
+ new['lastupdate'],
+ new['filmupdate'],
+ new['fullupdate'],
+ new['add_chn'],
+ new['add_shw'],
+ new['add_mov'],
+ new['del_chn'],
+ new['del_shw'],
+ new['del_mov'],
+ new['tot_chn'],
+ new['tot_shw'],
+ new['tot_mov'],
+ )
+ )
+ else:
+ # update status
+ cursor.execute(
+ """
+ UPDATE `status`
+ SET `modified` = %s,
+ `status` = %s,
+ `lastupdate` = %s,
+ `filmupdate` = %s,
+ `fullupdate` = %s,
+ `add_chn` = %s,
+ `add_shw` = %s,
+ `add_mov` = %s,
+ `del_chm` = %s,
+ `del_shw` = %s,
+ `del_mov` = %s,
+ `tot_chn` = %s,
+ `tot_shw` = %s,
+ `tot_mov` = %s
+ """, (
+ new['modified'],
+ new['status'],
+ new['lastupdate'],
+ new['filmupdate'],
+ new['fullupdate'],
+ new['add_chn'],
+ new['add_shw'],
+ new['add_mov'],
+ new['del_chn'],
+ new['del_shw'],
+ new['del_mov'],
+ new['tot_chn'],
+ new['tot_shw'],
+ new['tot_mov'],
+ )
+ )
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def SupportsUpdate( self ):
+ return True
+
+ def ftInit( self ):
+ # prevent concurrent updating
+ cursor = self.conn.cursor()
+ cursor.execute(
+ """
+ UPDATE `status`
+ SET `modified` = %s,
+ `status` = 'UPDATING'
+ WHERE ( `status` != 'UPDATING' )
+ OR
+ ( `modified` < %s )
+ """, (
+ int( time.time() ),
+ int( time.time() ) - 86400
+ )
+ )
+ retval = cursor.rowcount > 0
+ self.conn.commit()
+ cursor.close()
+ self.ft_channel = None
+ self.ft_channelid = None
+ self.ft_show = None
+ self.ft_showid = None
+ return retval
+
+ def ftUpdateStart( self, full ):
+ param = ( 1, ) if full else ( 0, )
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftUpdateStart', param )
+ for result in cursor.stored_results():
+ for ( cnt_chn, cnt_shw, cnt_mov ) in result:
+ cursor.close()
+ self.conn.commit()
+ return ( cnt_chn, cnt_shw, cnt_mov )
+ # should never happen
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, 0, )
+
+ def ftUpdateEnd( self, delete ):
+ param = ( 1, ) if delete else ( 0, )
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftUpdateEnd', param )
+ for result in cursor.stored_results():
+ for ( del_chn, del_shw, del_mov, cnt_chn, cnt_shw, cnt_mov ) in result:
+ cursor.close()
+ self.conn.commit()
+ return ( del_chn, del_shw, del_mov, cnt_chn, cnt_shw, cnt_mov )
+ # should never happen
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, 0, 0, 0, 0, )
+
+ def ftInsertFilm( self, film, commit ):
+ newchn = False
+ inschn = 0
+ insshw = 0
+ insmov = 0
+
+ # handle channel
+ if self.ft_channel != film['channel']:
+ # process changed channel
+ newchn = True
+ self.ft_channel = film['channel']
+ ( self.ft_channelid, inschn ) = self._insert_channel( self.ft_channel )
+ if self.ft_channelid == 0:
+ self.logger.info( 'Undefined error adding channel "{}"', self.ft_channel )
+ return ( 0, 0, 0, 0, )
+
+ if newchn or self.ft_show != film['show']:
+ # process changed show
+ self.ft_show = film['show']
+ ( self.ft_showid, insshw ) = self._insert_show( self.ft_channelid, self.ft_show, self._make_search( self.ft_show ) )
+ if self.ft_showid == 0:
+ self.logger.info( 'Undefined error adding show "{}"', self.ft_show )
+ return ( 0, 0, 0, 0, )
+
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftInsertFilm', (
+ self.ft_channelid,
+ self.ft_showid,
+ film["title"],
+ self._make_search( film['title'] ),
+ film["aired"],
+ film["duration"],
+ film["size"],
+ film["description"],
+ film["website"],
+ film["url_sub"],
+ film["url_video"],
+ film["url_video_sd"],
+ film["url_video_hd"],
+ film["airedepoch"],
+ ) )
+ for result in cursor.stored_results():
+ for ( filmid, insmov ) in result:
+ cursor.close()
+ if commit:
+ self.conn.commit()
+ return ( filmid, inschn, insshw, insmov )
+ # should never happen
+ cursor.close()
+ if commit:
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, 0, 0, )
+
+ def _insert_channel( self, channel ):
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftInsertChannel', ( channel, ) )
+ for result in cursor.stored_results():
+ for ( id, added ) in result:
+ cursor.close()
+ self.conn.commit()
+ return ( id, added )
+ # should never happen
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, )
+
+ def _insert_show( self, channelid, show, search ):
+ try:
+ cursor = self.conn.cursor()
+ cursor.callproc( 'ftInsertShow', ( channelid, show, search, ) )
+ for result in cursor.stored_results():
+ for ( id, added ) in result:
+ cursor.close()
+ self.conn.commit()
+ return ( id, added )
+ # should never happen
+ cursor.close()
+ self.conn.commit()
+ except mysql.connector.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return ( 0, 0, )
+
+ def _make_search( self, val ):
+ cset = string.letters + string.digits + ' _-#'
+ search = ''.join( [ c for c in val if c in cset ] )
+ return search.upper().strip()
diff --git a/plugin.video.mediathekview/classes/storesqlite.py b/plugin.video.mediathekview/classes/storesqlite.py
new file mode 100644
index 0000000..f73acdf
--- /dev/null
+++ b/plugin.video.mediathekview/classes/storesqlite.py
@@ -0,0 +1,766 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Leo Moll
+#
+
+# -- Imports ------------------------------------------------
+import os, stat, string, time
+import sqlite3
+
+from classes.film import Film
+from classes.exceptions import DatabaseCorrupted
+
+# -- Classes ------------------------------------------------
+class StoreSQLite( object ):
+ def __init__( self, logger, notifier, settings ):
+ self.logger = logger
+ self.notifier = notifier
+ self.settings = settings
+ # internals
+ self.conn = None
+ self.dbfile = os.path.join( self.settings.datapath, 'filmliste-v1.db' )
+ # useful query fragments
+ self.sql_query_films = "SELECT film.id,title,show,channel,description,duration,size,datetime(aired, 'unixepoch', 'localtime'),url_sub,url_video,url_video_sd,url_video_hd FROM film LEFT JOIN show ON show.id=film.showid LEFT JOIN channel ON channel.id=film.channelid"
+ self.sql_cond_recent = "( ( UNIX_TIMESTAMP() - aired ) <= 86400 )"
+ self.sql_cond_nofuture = " AND ( ( aired IS NULL ) OR ( ( UNIX_TIMESTAMP() - aired ) > 0 ) )" if settings.nofuture else ""
+ self.sql_cond_minlength = " AND ( ( duration IS NULL ) OR ( duration >= %d ) )" % settings.minlength if settings.minlength > 0 else ""
+
+ def Init( self, reset = False ):
+ self.logger.info( 'Using SQLite version {}, python library sqlite3 version {}', sqlite3.sqlite_version, sqlite3.version )
+ if not self._dir_exists( self.settings.datapath ):
+ os.mkdir( self.settings.datapath )
+ if reset == True or not self._file_exists( self.dbfile ):
+ self.logger.info( '===== RESET: Database will be deleted and regenerated =====' )
+ self._file_remove( self.dbfile )
+ self.conn = sqlite3.connect( self.dbfile, timeout = 60 )
+ self._handle_database_initialization()
+ else:
+ try:
+ self.conn = sqlite3.connect( self.dbfile, timeout = 60 )
+ except sqlite3.DatabaseError as err:
+ self.logger.error( 'Errore while opening database. Trying to fully reset the Database...' )
+ self.Init( reset = True )
+
+ self.conn.execute( 'pragma journal_mode=off' ) # 3x speed-up, check mode 'WAL'
+ self.conn.execute( 'pragma synchronous=off' ) # that is a bit dangerous :-) but faaaast
+
+ self.conn.create_function( 'UNIX_TIMESTAMP', 0, UNIX_TIMESTAMP )
+ self.conn.create_aggregate( 'GROUP_CONCAT', 1, GROUP_CONCAT )
+
+ def Exit( self ):
+ if self.conn is not None:
+ self.conn.close()
+ self.conn = None
+
+ def Search( self, search, filmui ):
+ self._Search_Condition( '( ( title LIKE "%%%s%%" ) OR ( show LIKE "%%%s%%" ) )' % ( search, search ), filmui, True, True )
+
+ def SearchFull( self, search, filmui ):
+ self._Search_Condition( '( ( title LIKE "%%%s%%" ) OR ( show LIKE "%%%s%%" ) OR ( description LIKE "%%%s%%") )' % ( search, search, search ), filmui, True, True )
+
+ def GetRecents( self, channelid, filmui ):
+ sql_cond_channel = ' AND ( film.channelid=' + str( channelid ) + ' ) ' if channelid != '0' else ''
+ self._Search_Condition( self.sql_cond_recent + sql_cond_channel, filmui, True, False )
+
+ def GetLiveStreams( self, filmui ):
+ self._Search_Condition( '( show.search="LIVESTREAM" )', filmui, False, False )
+
+ def GetChannels( self, channelui ):
+ self._Channels_Condition( None, channelui )
+
+ def GetRecentChannels( self, channelui ):
+ self._Channels_Condition( self.sql_cond_recent, channelui )
+
+ def _Channels_Condition( self, condition, channelui):
+ if self.conn is None:
+ return
+ try:
+ if condition is None:
+ query = 'SELECT id,channel,0 AS `count` FROM channel'
+ else:
+ query = 'SELECT channel.id AS `id`,channel,COUNT(*) AS `count` FROM film LEFT JOIN channel ON channel.id=film.channelid WHERE ' + condition + ' GROUP BY channel'
+ self.logger.info( 'SQLite Query: {}', query )
+ cursor = self.conn.cursor()
+ cursor.execute( query )
+ channelui.Begin()
+ for ( channelui.id, channelui.channel, channelui.count ) in cursor:
+ channelui.Add()
+ channelui.End()
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetInitials( self, channelid, initialui ):
+ if self.conn is None:
+ return
+ try:
+ condition = 'WHERE ( channelid=' + str( channelid ) + ' ) ' if channelid != '0' else ''
+ self.logger.info( 'SQlite Query: {}',
+ 'SELECT SUBSTR(search,1,1),COUNT(*) FROM show ' +
+ condition +
+ 'GROUP BY LEFT(search,1)'
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ 'SELECT SUBSTR(search,1,1),COUNT(*) FROM show ' +
+ condition +
+ 'GROUP BY SUBSTR(search,1,1)'
+ )
+ initialui.Begin( channelid )
+ for ( initialui.initial, initialui.count ) in cursor:
+ initialui.Add()
+ initialui.End()
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetShows( self, channelid, initial, showui ):
+ if self.conn is None:
+ return
+ try:
+ if channelid == '0' and self.settings.groupshows:
+ query = 'SELECT GROUP_CONCAT(show.id),GROUP_CONCAT(channelid),show,GROUP_CONCAT(channel) FROM show LEFT JOIN channel ON channel.id=show.channelid WHERE ( show LIKE "%s%%" ) GROUP BY show' % initial
+ elif channelid == '0':
+ query = 'SELECT show.id,show.channelid,show.show,channel.channel FROM show LEFT JOIN channel ON channel.id=show.channelid WHERE ( show LIKE "%s%%" )' % initial
+ else:
+ query = 'SELECT show.id,show.channelid,show.show,channel.channel FROM show LEFT JOIN channel ON channel.id=show.channelid WHERE ( channelid=%s ) AND ( show LIKE "%s%%" )' % ( channelid, initial )
+ self.logger.info( 'SQLite Query: {}', query )
+ cursor = self.conn.cursor()
+ cursor.execute( query )
+ showui.Begin( channelid )
+ for ( showui.id, showui.channelid, showui.show, showui.channel ) in cursor:
+ showui.Add()
+ showui.End()
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def GetFilms( self, showid, filmui ):
+ if self.conn is None:
+ return
+ if showid.find( ',' ) == -1:
+ # only one channel id
+ condition = '( showid=%s )' % showid
+ showchannels = False
+ else:
+ # multiple channel ids
+ condition = '( showid IN ( %s ) )' % showid
+ showchannels = True
+ self._Search_Condition( condition, filmui, False, showchannels )
+
+ def _Search_Condition( self, condition, filmui, showshows, showchannels ):
+ if self.conn is None:
+ return
+ try:
+ self.logger.info( 'SQLite Query: {}',
+ self.sql_query_films +
+ ' WHERE ' +
+ condition +
+ self.sql_cond_nofuture +
+ self.sql_cond_minlength
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ self.sql_query_films +
+ ' WHERE ' +
+ condition +
+ self.sql_cond_nofuture +
+ self.sql_cond_minlength
+ )
+ filmui.Begin( showshows, showchannels )
+ for ( filmui.id, filmui.title, filmui.show, filmui.channel, filmui.description, filmui.seconds, filmui.size, filmui.aired, filmui.url_sub, filmui.url_video, filmui.url_video_sd, filmui.url_video_hd ) in cursor:
+ filmui.Add()
+ filmui.End()
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+
+ def RetrieveFilmInfo( self, filmid ):
+ if self.conn is None:
+ return None
+ try:
+ condition = '( film.id={} )'.format( filmid )
+ self.logger.info( 'SQLite Query: {}',
+ self.sql_query_films +
+ ' WHERE ' +
+ condition
+ )
+ cursor = self.conn.cursor()
+ cursor.execute(
+ self.sql_query_films +
+ ' WHERE ' +
+ condition
+ )
+ film = Film()
+ for ( film.id, film.title, film.show, film.channel, film.description, film.seconds, film.size, film.aired, film.url_sub, film.url_video, film.url_video_sd, film.url_video_hd ) in cursor:
+ cursor.close()
+ return film
+ cursor.close()
+ except sqlite3.Error as err:
+ self.logger.error( 'Database error: {}', err )
+ self.notifier.ShowDatabaseError( err )
+ return None
+
+ def GetStatus( self ):
+ status = {
+ 'modified': int( time.time() ),
+ 'status': '',
+ 'lastupdate': 0,
+ 'filmupdate': 0,
+ 'fullupdate': 0,
+ 'add_chn': 0,
+ 'add_shw': 0,
+ 'add_mov': 0,
+ 'del_chn': 0,
+ 'del_shw': 0,
+ 'del_mov': 0,
+ 'tot_chn': 0,
+ 'tot_shw': 0,
+ 'tot_mov': 0
+ }
+ if self.conn is None:
+ status['status'] = "UNINIT"
+ return status
+ self.conn.commit()
+ cursor = self.conn.cursor()
+ cursor.execute( 'SELECT * FROM `status` LIMIT 1' )
+ r = cursor.fetchall()
+ cursor.close()
+ if len( r ) == 0:
+ status['status'] = "NONE"
+ return status
+ status['modified'] = r[0][0]
+ status['status'] = r[0][1]
+ status['lastupdate'] = r[0][2]
+ status['filmupdate'] = r[0][3]
+ status['fullupdate'] = r[0][4]
+ status['add_chn'] = r[0][5]
+ status['add_shw'] = r[0][6]
+ status['add_mov'] = r[0][7]
+ status['del_chn'] = r[0][8]
+ status['del_shw'] = r[0][9]
+ status['del_mov'] = r[0][10]
+ status['tot_chn'] = r[0][11]
+ status['tot_shw'] = r[0][12]
+ status['tot_mov'] = r[0][13]
+ return status
+
+ def UpdateStatus( self, status = None, lastupdate = None, filmupdate = None, fullupdate = None, add_chn = None, add_shw = None, add_mov = None, del_chn = None, del_shw = None, del_mov = None, tot_chn = None, tot_shw = None, tot_mov = None ):
+ if self.conn is None:
+ return
+ new = self.GetStatus()
+ old = new['status']
+ if status is not None:
+ new['status'] = status
+ if lastupdate is not None:
+ new['lastupdate'] = lastupdate
+ if filmupdate is not None:
+ new['filmupdate'] = filmupdate
+ if fullupdate is not None:
+ new['fullupdate'] = fullupdate
+ if add_chn is not None:
+ new['add_chn'] = add_chn
+ if add_shw is not None:
+ new['add_shw'] = add_shw
+ if add_mov is not None:
+ new['add_mov'] = add_mov
+ if del_chn is not None:
+ new['del_chn'] = del_chn
+ if del_shw is not None:
+ new['del_shw'] = del_shw
+ if del_mov is not None:
+ new['del_mov'] = del_mov
+ if tot_chn is not None:
+ new['tot_chn'] = tot_chn
+ if tot_shw is not None:
+ new['tot_shw'] = tot_shw
+ if tot_mov is not None:
+ new['tot_mov'] = tot_mov
+ # TODO: we should only write, if we have changed something...
+ new['modified'] = int( time.time() )
+ cursor = self.conn.cursor()
+ if old == "NONE":
+ # insert status
+ cursor.execute(
+ """
+ INSERT INTO `status` (
+ `modified`,
+ `status`,
+ `lastupdate`,
+ `filmupdate`,
+ `fullupdate`,
+ `add_chn`,
+ `add_shw`,
+ `add_mov`,
+ `del_chm`,
+ `del_shw`,
+ `del_mov`,
+ `tot_chn`,
+ `tot_shw`,
+ `tot_mov`
+ )
+ VALUES (
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?
+ )
+ """, (
+ new['modified'],
+ new['status'],
+ new['lastupdate'],
+ new['filmupdate'],
+ new['fullupdate'],
+ new['add_chn'],
+ new['add_shw'],
+ new['add_mov'],
+ new['del_chn'],
+ new['del_shw'],
+ new['del_mov'],
+ new['tot_chn'],
+ new['tot_shw'],
+ new['tot_mov'],
+ )
+ )
+ else:
+ # update status
+ cursor.execute(
+ """
+ UPDATE `status`
+ SET `modified` = ?,
+ `status` = ?,
+ `lastupdate` = ?,
+ `filmupdate` = ?,
+ `fullupdate` = ?,
+ `add_chn` = ?,
+ `add_shw` = ?,
+ `add_mov` = ?,
+ `del_chm` = ?,
+ `del_shw` = ?,
+ `del_mov` = ?,
+ `tot_chn` = ?,
+ `tot_shw` = ?,
+ `tot_mov` = ?
+ """, (
+ new['modified'],
+ new['status'],
+ new['lastupdate'],
+ new['filmupdate'],
+ new['fullupdate'],
+ new['add_chn'],
+ new['add_shw'],
+ new['add_mov'],
+ new['del_chn'],
+ new['del_shw'],
+ new['del_mov'],
+ new['tot_chn'],
+ new['tot_shw'],
+ new['tot_mov'],
+ )
+ )
+ cursor.close()
+ self.conn.commit()
+
+ def SupportsUpdate( self ):
+ return True
+
+ def ftInit( self ):
+ try:
+ # prevent concurrent updating
+ self.conn.commit()
+ cursor = self.conn.cursor()
+ cursor.execute(
+ """
+ UPDATE `status`
+ SET `modified` = ?,
+ `status` = 'UPDATING'
+ WHERE ( `status` != 'UPDATING' )
+ OR
+ ( `modified` < ? )
+ """, (
+ int( time.time() ),
+ int( time.time() ) - 86400
+ )
+ )
+ retval = cursor.rowcount > 0
+ self.conn.commit()
+ cursor.close()
+ self.ft_channel = None
+ self.ft_channelid = None
+ self.ft_show = None
+ self.ft_showid = None
+ return retval
+ except sqlite3.DatabaseError as err:
+ self._handle_database_corruption( err )
+ raise DatabaseCorrupted( 'Database error during critical operation: {} - Database will be rebuilt from scratch.'.format( err ) )
+
+ def ftUpdateStart( self, full ):
+ try:
+ cursor = self.conn.cursor()
+ if full:
+ cursor.executescript( """
+ UPDATE `channel`
+ SET `touched` = 0;
+
+ UPDATE `show`
+ SET `touched` = 0;
+
+ UPDATE `film`
+ SET `touched` = 0;
+ """ )
+ cursor.execute( 'SELECT COUNT(*) FROM `channel`' )
+ r1 = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `show`' )
+ r2 = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `film`' )
+ r3 = cursor.fetchone()
+ cursor.close()
+ self.conn.commit()
+ return ( r1[0], r2[0], r3[0], )
+ except sqlite3.DatabaseError as err:
+ self._handle_database_corruption( err )
+ raise DatabaseCorrupted( 'Database error during critical operation: {} - Database will be rebuilt from scratch.'.format( err ) )
+
+ def ftUpdateEnd( self, delete ):
+ try:
+ cursor = self.conn.cursor()
+ cursor.execute( 'SELECT COUNT(*) FROM `channel` WHERE ( touched = 0 )' )
+ ( del_chn, ) = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `show` WHERE ( touched = 0 )' )
+ ( del_shw, ) = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `film` WHERE ( touched = 0 )' )
+ ( del_mov, ) = cursor.fetchone()
+ if delete:
+ cursor.execute( 'DELETE FROM `show` WHERE ( show.touched = 0 ) AND ( ( SELECT SUM( film.touched ) FROM `film` WHERE film.showid = show.id ) = 0 )' )
+ cursor.execute( 'DELETE FROM `film` WHERE ( touched = 0 )' )
+ else:
+ del_chn = 0
+ del_shw = 0
+ del_mov = 0
+ cursor.execute( 'SELECT COUNT(*) FROM `channel`' )
+ ( cnt_chn, ) = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `show`' )
+ ( cnt_shw, ) = cursor.fetchone()
+ cursor.execute( 'SELECT COUNT(*) FROM `film`' )
+ ( cnt_mov, ) = cursor.fetchone()
+ cursor.close()
+ self.conn.commit()
+ return ( del_chn, del_shw, del_mov, cnt_chn, cnt_shw, cnt_mov, )
+ except sqlite3.DatabaseError as err:
+ self._handle_database_corruption( err )
+ raise DatabaseCorrupted( 'Database error during critical operation: {} - Database will be rebuilt from scratch.'.format( err ) )
+
+ def ftInsertFilm( self, film, commit ):
+ try:
+ cursor = self.conn.cursor()
+ newchn = False
+ inschn = 0
+ insshw = 0
+ insmov = 0
+
+ # handle channel
+ if self.ft_channel != film['channel']:
+ # process changed channel
+ newchn = True
+ cursor.execute( 'SELECT `id`,`touched` FROM `channel` WHERE channel.channel=?', ( film['channel'], ) )
+ r = cursor.fetchall()
+ if len( r ) > 0:
+ # get the channel data
+ self.ft_channel = film['channel']
+ self.ft_channelid = r[0][0]
+ if r[0][1] == 0:
+ # updated touched
+ cursor.execute( 'UPDATE `channel` SET `touched`=1 WHERE ( channel.id=? )', ( self.ft_channelid, ) )
+ else:
+ # insert the new channel
+ inschn = 1
+ cursor.execute( 'INSERT INTO `channel` ( `dtCreated`,`channel` ) VALUES ( ?,? )', ( int( time.time() ), film['channel'] ) )
+ self.ft_channel = film['channel']
+ self.ft_channelid = cursor.lastrowid
+
+ # handle show
+ if newchn or self.ft_show != film['show']:
+ # process changed show
+ cursor.execute( 'SELECT `id`,`touched` FROM `show` WHERE ( show.channelid=? ) AND ( show.show=? )', ( self.ft_channelid, film['show'] ) )
+ r = cursor.fetchall()
+ if len( r ) > 0:
+ # get the show data
+ self.ft_show = film['show']
+ self.ft_showid = r[0][0]
+ if r[0][1] == 0:
+ # updated touched
+ cursor.execute( 'UPDATE `show` SET `touched`=1 WHERE ( show.id=? )', ( self.ft_showid, ) )
+ else:
+ # insert the new show
+ insshw = 1
+ cursor.execute(
+ """
+ INSERT INTO `show` (
+ `dtCreated`,
+ `channelid`,
+ `show`,
+ `search`
+ )
+ VALUES (
+ ?,
+ ?,
+ ?,
+ ?
+ )
+ """, (
+ int( time.time() ),
+ self.ft_channelid, film['show'],
+ self._make_search( film['show'] )
+ )
+ )
+ self.ft_show = film['show']
+ self.ft_showid = cursor.lastrowid
+
+ # check if the movie is there
+ cursor.execute( """
+ SELECT `id`,
+ `touched`
+ FROM `film`
+ WHERE ( film.channelid = ? )
+ AND
+ ( film.showid = ? )
+ AND
+ ( film.url_video = ? )
+ """, ( self.ft_channelid, self.ft_showid, film['url_video'] ) )
+ r = cursor.fetchall()
+ if len( r ) > 0:
+ # film found
+ filmid = r[0][0]
+ if r[0][1] == 0:
+ # update touched
+ cursor.execute( 'UPDATE `film` SET `touched`=1 WHERE ( film.id=? )', ( filmid, ) )
+ else:
+ # insert the new film
+ insmov = 1
+ cursor.execute(
+ """
+ INSERT INTO `film` (
+ `dtCreated`,
+ `channelid`,
+ `showid`,
+ `title`,
+ `search`,
+ `aired`,
+ `duration`,
+ `size`,
+ `description`,
+ `website`,
+ `url_sub`,
+ `url_video`,
+ `url_video_sd`,
+ `url_video_hd`
+ )
+ VALUES (
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?,
+ ?
+ )
+ """, (
+ int( time.time() ),
+ self.ft_channelid,
+ self.ft_showid,
+ film['title'],
+ self._make_search( film['title'] ),
+ film['airedepoch'],
+ self._make_duration( film['duration'] ),
+ film['size'],
+ film['description'],
+ film['website'],
+ film['url_sub'],
+ film['url_video'],
+ film['url_video_sd'],
+ film['url_video_hd']
+ )
+ )
+ filmid = cursor.lastrowid
+ if commit:
+ self.conn.commit()
+ cursor.close()
+ return ( filmid, inschn, insshw, insmov )
+ except sqlite3.DatabaseError as err:
+ self._handle_database_corruption( err )
+ raise DatabaseCorrupted( 'Database error during critical operation: {} - Database will be rebuilt from scratch.'.format( err ) )
+
+ def _handle_database_corruption( self, err ):
+ self.logger.error( 'Database error during critical operation: {} - Database will be rebuilt from scratch.', err )
+ self.notifier.ShowDatabaseError( err )
+ self.Exit()
+ self.Init( reset = True )
+
+ def _handle_database_initialization( self ):
+ self.conn.executescript( """
+PRAGMA foreign_keys = false;
+
+-- ----------------------------
+-- Table structure for channel
+-- ----------------------------
+DROP TABLE IF EXISTS "channel";
+CREATE TABLE "channel" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "dtCreated" integer(11,0) NOT NULL DEFAULT 0,
+ "touched" integer(1,0) NOT NULL DEFAULT 1,
+ "channel" TEXT(255,0) NOT NULL
+);
+
+-- ----------------------------
+-- Table structure for film
+-- ----------------------------
+DROP TABLE IF EXISTS "film";
+CREATE TABLE "film" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "dtCreated" integer(11,0) NOT NULL DEFAULT 0,
+ "touched" integer(1,0) NOT NULL DEFAULT 1,
+ "channelid" INTEGER(11,0) NOT NULL,
+ "showid" INTEGER(11,0) NOT NULL,
+ "title" TEXT(255,0) NOT NULL,
+ "search" TEXT(255,0) NOT NULL,
+ "aired" integer(11,0),
+ "duration" integer(11,0),
+ "size" integer(11,0),
+ "description" TEXT(2048,0),
+ "website" TEXT(384,0),
+ "url_sub" TEXT(384,0),
+ "url_video" TEXT(384,0),
+ "url_video_sd" TEXT(384,0),
+ "url_video_hd" TEXT(384,0),
+ CONSTRAINT "FK_FilmShow" FOREIGN KEY ("showid") REFERENCES "show" ("id") ON DELETE CASCADE,
+ CONSTRAINT "FK_FilmChannel" FOREIGN KEY ("channelid") REFERENCES "channel" ("id") ON DELETE CASCADE
+);
+
+-- ----------------------------
+-- Table structure for show
+-- ----------------------------
+DROP TABLE IF EXISTS "show";
+CREATE TABLE "show" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "dtCreated" integer(11,0) NOT NULL DEFAULT 0,
+ "touched" integer(1,0) NOT NULL DEFAULT 1,
+ "channelid" INTEGER(11,0) NOT NULL DEFAULT 0,
+ "show" TEXT(255,0) NOT NULL,
+ "search" TEXT(255,0) NOT NULL,
+ CONSTRAINT "FK_ShowChannel" FOREIGN KEY ("channelid") REFERENCES "channel" ("id") ON DELETE CASCADE
+);
+
+-- ----------------------------
+-- Table structure for status
+-- ----------------------------
+DROP TABLE IF EXISTS "status";
+CREATE TABLE "status" (
+ "modified" integer(11,0),
+ "status" TEXT(32,0),
+ "lastupdate" integer(11,0),
+ "filmupdate" integer(11,0),
+ "fullupdate" integer(1,0),
+ "add_chn" integer(11,0),
+ "add_shw" integer(11,0),
+ "add_mov" integer(11,0),
+ "del_chm" integer(11,0),
+ "del_shw" integer(11,0),
+ "del_mov" integer(11,0),
+ "tot_chn" integer(11,0),
+ "tot_shw" integer(11,0),
+ "tot_mov" integer(11,0)
+);
+
+-- ----------------------------
+-- Indexes structure for table film
+-- ----------------------------
+CREATE INDEX "dupecheck" ON film ("channelid", "showid", "url_video");
+CREATE INDEX "index_1" ON film ("channelid", "title" COLLATE NOCASE);
+CREATE INDEX "index_2" ON film ("showid", "title" COLLATE NOCASE);
+
+-- ----------------------------
+-- Indexes structure for table show
+-- ----------------------------
+CREATE INDEX "category" ON show ("category");
+CREATE INDEX "search" ON show ("search");
+CREATE INDEX "combined_1" ON show ("channelid", "search");
+CREATE INDEX "combined_2" ON show ("channelid", "show");
+
+PRAGMA foreign_keys = true;
+ """ )
+ self.UpdateStatus( 'IDLE' )
+
+ def _make_search( self, val ):
+ cset = string.letters + string.digits + ' _-#'
+ search = ''.join( [ c for c in val if c in cset ] )
+ return search.upper().strip()
+
+ def _make_duration( self, val ):
+ if val == "00:00:00":
+ return None
+ elif val is None:
+ return None
+ x = val.split( ':' )
+ if len( x ) != 3:
+ return None
+ return int( x[0] ) * 3600 + int( x[1] ) * 60 + int( x[2] )
+
+ def _dir_exists( self, name ):
+ try:
+ s = os.stat( name )
+ return stat.S_ISDIR( s.st_mode )
+ except OSError as err:
+ return False
+
+ def _file_exists( self, name ):
+ try:
+ s = os.stat( name )
+ return stat.S_ISREG( s.st_mode )
+ except OSError as err:
+ return False
+
+ def _file_remove( self, name ):
+ if self._file_exists( name ):
+ try:
+ os.remove( name )
+ return True
+ except OSError as err:
+ self.logger.error( 'Failed to remove {}: error {}', name, err )
+ return False
+
+def UNIX_TIMESTAMP():
+ return int( time.time() )
+
+class GROUP_CONCAT:
+ def __init__( self ):
+ self.value = ''
+
+ def step( self, value ):
+ if value is not None:
+ if self.value == '':
+ self.value = '{0}'.format( value )
+ else:
+ self.value = '{0},{1}'.format( self.value, value )
+
+ def finalize(self):
+ return self.value
diff --git a/plugin.video.mediathekview/classes/ttml2srt.py b/plugin.video.mediathekview/classes/ttml2srt.py
new file mode 100644
index 0000000..e36b41e
--- /dev/null
+++ b/plugin.video.mediathekview/classes/ttml2srt.py
@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Laura Klünder
+# See https://github.com/codingcatgirl/ttml2srt
+#
+# MIT License
+#
+# Copyright (c) 2017 Laura Klünder
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import re
+import io
+import sys
+from datetime import timedelta
+from xml.etree import ElementTree as ET
+
+def ttml2srt( infile, outfile ):
+ tree = ET.parse( infile )
+ root = tree.getroot()
+
+ # strip namespaces
+ for elem in root.getiterator():
+ elem.tag = elem.tag.split('}', 1)[-1]
+ elem.attrib = {name.split('}', 1)
+ [-1]: value for name, value in elem.attrib.items()}
+
+ # get styles
+ styles = {}
+ for elem in root.findall('./head/styling/style'):
+ style = {}
+ if 'color' in elem.attrib:
+ color = elem.attrib['color']
+ if color not in ('#FFFFFF', '#000000'):
+ style['color'] = color
+ if 'fontStyle' in elem.attrib:
+ fontstyle = elem.attrib['fontStyle']
+ if fontstyle in ('italic', ):
+ style['fontstyle'] = fontstyle
+ styles[elem.attrib['id']] = style
+
+ body = root.find('./body')
+
+ # parse correct start and end times
+ def parse_time_expression(expression, default_offset=timedelta(0)):
+ offset_time = re.match(r'^([0-9]+(\.[0-9]+)?)(h|m|s|ms|f|t)$', expression)
+ if offset_time:
+ time_value, fraction, metric = offset_time.groups()
+ time_value = float(time_value)
+ if metric == 'h':
+ return default_offset + timedelta(hours=time_value)
+ elif metric == 'm':
+ return default_offset + timedelta(minutes=time_value)
+ elif metric == 's':
+ return default_offset + timedelta(seconds=time_value)
+ elif metric == 'ms':
+ return default_offset + timedelta(milliseconds=time_value)
+ elif metric == 'f':
+ raise NotImplementedError(
+ 'Parsing time expressions by frame is not supported!')
+ elif metric == 't':
+ raise NotImplementedError(
+ 'Parsing time expressions by ticks is not supported!')
+
+ clock_time = re.match(
+ r'^([0-9]{2,}):([0-9]{2,}):([0-9]{2,}(\.[0-9]+)?)$', expression)
+ if clock_time:
+ hours, minutes, seconds, fraction = clock_time.groups()
+ return timedelta(hours=int(hours), minutes=int(minutes), seconds=float(seconds))
+
+ clock_time_frames = re.match(
+ r'^([0-9]{2,}):([0-9]{2,}):([0-9]{2,}):([0-9]{2,}(\.[0-9]+)?)$', expression)
+ if clock_time_frames:
+ raise NotImplementedError(
+ 'Parsing time expressions by frame is not supported!')
+
+ raise ValueError('unknown time expression: %s' % expression)
+
+
+ def parse_times(elem, default_begin=timedelta(0)):
+ if 'begin' in elem.attrib:
+ begin = parse_time_expression(
+ elem.attrib['begin'], default_offset=default_begin)
+ else:
+ begin = default_begin
+ elem.attrib['{abs}begin'] = begin
+
+ end = None
+ if 'end' in elem.attrib:
+ end = parse_time_expression(
+ elem.attrib['end'], default_offset=default_begin)
+
+ dur = None
+ if 'dur' in elem.attrib:
+ dur = parse_time_expression(elem.attrib['dur'])
+
+ if dur is not None:
+ if end is None:
+ end = begin + dur
+ else:
+ end = min(end, begin + dur)
+
+ elem.attrib['{abs}end'] = end
+
+ for child in elem:
+ parse_times(child, default_begin=begin)
+
+
+ parse_times(body)
+
+ timestamps = set()
+ for elem in body.findall('.//*[@{abs}begin]'):
+ timestamps.add(elem.attrib['{abs}begin'])
+
+ for elem in body.findall('.//*[@{abs}end]'):
+ timestamps.add(elem.attrib['{abs}end'])
+
+ timestamps.discard(None)
+
+ # render subtitles on each timestamp
+
+
+ def render_subtitles(elem, timestamp, parent_style={}):
+
+ if timestamp < elem.attrib['{abs}begin']:
+ return ''
+ if elem.attrib['{abs}end'] is not None and timestamp >= elem.attrib['{abs}end']:
+ return ''
+
+ result = ''
+
+ style = parent_style.copy()
+ if 'style' in elem.attrib:
+ style.update(styles[elem.attrib['style']])
+
+ if 'color' in style:
+ result += '<font color="%s">' % style['color']
+
+ if style.get('fontstyle') == 'italic':
+ result += '<i>'
+
+ if elem.text:
+ result += elem.text.strip()
+ if len(elem):
+ for child in elem:
+ result += render_subtitles(child, timestamp)
+ if child.tail:
+ result += child.tail.strip()
+
+ if 'color' in style:
+ result += '</font>'
+
+ if style.get('fontstyle') == 'italic':
+ result += '</i>'
+
+ if elem.tag in ('div', 'p', 'br'):
+ result += '\n'
+
+ return result
+
+
+ rendered = []
+ for timestamp in sorted(timestamps):
+ rendered.append((timestamp, re.sub(r'\n\n\n+', '\n\n',
+ render_subtitles(body, timestamp)).strip()))
+
+ if not rendered:
+ exit(0)
+
+ # group timestamps together if nothing changes
+ rendered_grouped = []
+ last_text = None
+ for timestamp, content in rendered:
+ if content != last_text:
+ rendered_grouped.append((timestamp, content))
+ last_text = content
+
+ # output srt
+ rendered_grouped.append((rendered_grouped[-1][0] + timedelta(hours=24), ''))
+
+
+ def format_timestamp(timestamp):
+ return ('%02d:%02d:%02.3f' % (timestamp.total_seconds() // 3600,
+ timestamp.total_seconds() // 60 % 60,
+ timestamp.total_seconds() % 60)).replace('.', ',')
+
+
+ if type( outfile ) is str or type( outfile ) is unicode:
+ file = io.open( outfile, 'w', encoding='utf-8' )
+ else:
+ file = outfile
+
+ srt_i = 1
+ for i, (timestamp, content) in enumerate(rendered_grouped[:-1]):
+ if content == '':
+ continue
+ file.write( bytearray( '%d\n' % srt_i, 'utf-8' ) )
+ file.write( bytearray(
+ format_timestamp( timestamp ) +
+ ' --> ' +
+ format_timestamp( rendered_grouped[i + 1][0] ) +
+ '\n'
+ ) )
+ file.write( bytearray( content + '\n\n', 'utf-8' ) )
+ srt_i += 1
+ file.close()
diff --git a/plugin.video.mediathekview/classes/updater.py b/plugin.video.mediathekview/classes/updater.py
new file mode 100644
index 0000000..8609303
--- /dev/null
+++ b/plugin.video.mediathekview/classes/updater.py
@@ -0,0 +1,431 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2018, Leo Moll
+
+# -- Imports ------------------------------------------------
+import os, stat, urllib, urllib2, subprocess, ijson, datetime, time
+import xml.etree.ElementTree as etree
+
+from operator import itemgetter
+from classes.store import Store
+from classes.exceptions import DatabaseCorrupted
+from classes.exceptions import DatabaseLost
+
+# -- Constants ----------------------------------------------
+FILMLISTE_AKT_URL = 'https://res.mediathekview.de/akt.xml'
+FILMLISTE_DIF_URL = 'https://res.mediathekview.de/diff.xml'
+
+# -- Classes ------------------------------------------------
+class MediathekViewUpdater( object ):
+ def __init__( self, logger, notifier, settings, monitor = None ):
+ self.logger = logger
+ self.notifier = notifier
+ self.settings = settings
+ self.monitor = monitor
+ self.db = None
+
+ def Init( self ):
+ if self.db is not None:
+ self.Exit()
+ self.db = Store( self.logger, self.notifier, self.settings )
+ self.db.Init()
+
+ def Exit( self ):
+ if self.db is not None:
+ self.db.Exit()
+ del self.db
+ self.db = None
+
+ def PrerequisitesMissing( self ):
+ return self.settings.updenabled and self._find_xz() is None
+
+ def IsEnabled( self ):
+ if self.settings.updenabled:
+ xz = self._find_xz()
+ return xz is not None
+
+ def GetCurrentUpdateOperation( self ):
+ if not self.IsEnabled() or self.db is None:
+ # update disabled or not possible
+ self.logger.info( 'update disabled or not possible' )
+ return 0
+ status = self.db.GetStatus()
+ tsnow = int( time.time() )
+ tsold = status['lastupdate']
+ dtnow = datetime.datetime.fromtimestamp( tsnow ).date()
+ dtold = datetime.datetime.fromtimestamp( tsold ).date()
+ if status['status'] == 'UNINIT':
+ # database not initialized
+ self.logger.debug( 'database not initialized' )
+ return 0
+ elif status['status'] == "UPDATING" and tsnow - tsold > 86400:
+ # process was probably killed during update
+ self.logger.info( 'Stuck update pretending to run since epoch {} reset', tsold )
+ self.db.UpdateStatus( 'ABORTED' )
+ return 0
+ elif status['status'] == "UPDATING":
+ # already updating
+ self.logger.debug( 'already updating' )
+ return 0
+ elif tsnow - tsold < self.settings.updinterval:
+ # last update less than the configured update interval. do nothing
+ self.logger.debug( 'last update less than the configured update interval. do nothing' )
+ return 0
+ elif dtnow != dtold:
+ # last update was not today. do full update once a day
+ self.logger.debug( 'last update was not today. do full update once a day' )
+ return 1
+ elif status['status'] == "ABORTED" and status['fullupdate'] == 1:
+ # last full update was aborted - full update needed
+ self.logger.debug( 'last full update was aborted - full update needed' )
+ return 1
+ else:
+ # do differential update
+ self.logger.debug( 'do differential update' )
+ return 2
+
+ def Update( self, full ):
+ if self.db is None:
+ return
+ if self.db.SupportsUpdate():
+ if self.GetNewestList( full ):
+ self.Import( full )
+
+ def Import( self, full ):
+ ( url, compfile, destfile, avgrecsize ) = self._get_update_info( full )
+ if not self._file_exists( destfile ):
+ self.logger.error( 'File {} does not exists', destfile )
+ return False
+ # estimate number of records in update file
+ records = int( self._file_size( destfile ) / avgrecsize )
+ if not self.db.ftInit():
+ self.logger.warn( 'Failed to initialize update. Maybe a concurrency problem?' )
+ return False
+ try:
+ self.logger.info( 'Starting import of approx. {} records from {}', records, destfile )
+ file = open( destfile, 'r' )
+ parser = ijson.parse( file )
+ flsm = 0
+ flts = 0
+ ( self.tot_chn, self.tot_shw, self.tot_mov ) = self._update_start( full )
+ self.notifier.ShowUpdateProgress()
+ for prefix, event, value in parser:
+ if ( prefix, event ) == ( "X", "start_array" ):
+ self._init_record()
+ elif ( prefix, event ) == ( "X", "end_array" ):
+ self._end_record( records )
+ if self.count % 100 == 0 and self.monitor.abortRequested():
+ # kodi is shutting down. Close all
+ file.close()
+ self._update_end( full, 'ABORTED' )
+ self.notifier.CloseUpdateProgress()
+ return True
+ elif ( prefix, event ) == ( "X.item", "string" ):
+ if value is not None:
+# self._add_value( value.strip().encode('utf-8') )
+ self._add_value( value.strip() )
+ else:
+ self._add_value( "" )
+ elif ( prefix, event ) == ( "Filmliste", "start_array" ):
+ flsm += 1
+ elif ( prefix, event ) == ( "Filmliste.item", "string" ):
+ flsm += 1
+ if flsm == 2 and value is not None:
+ # this is the timestmap of this database update
+ try:
+ fldt = datetime.datetime.strptime( value.strip(), "%d.%m.%Y, %H:%M" )
+ flts = int( time.mktime( fldt.timetuple() ) )
+ self.db.UpdateStatus( filmupdate = flts )
+ self.logger.info( 'Filmliste dated {}', value.strip() )
+ except TypeError:
+ # SEE: https://forum.kodi.tv/showthread.php?tid=112916&pid=1214507#pid1214507
+ # Wonderful. His name is also Leopold
+ try:
+ flts = int( time.mktime( time.strptime( value.strip(), "%d.%m.%Y, %H:%M" ) ) )
+ self.db.UpdateStatus( filmupdate = flts )
+ self.logger.info( 'Filmliste dated {}', value.strip() )
+ except Exception as err:
+ # If the universe hates us...
+ pass
+ except ValueError as err:
+ pass
+
+ file.close()
+ self._update_end( full, 'IDLE' )
+ self.logger.info( 'Import of {} finished', destfile )
+ self.notifier.CloseUpdateProgress()
+ return True
+ except KeyboardInterrupt:
+ file.close()
+ self._update_end( full, 'ABORTED' )
+ self.logger.info( 'Interrupted by user' )
+ self.notifier.CloseUpdateProgress()
+ return True
+ except DatabaseCorrupted as err:
+ self.logger.error( '{}', err )
+ self.notifier.CloseUpdateProgress()
+ file.close()
+ except DatabaseLost as err:
+ self.logger.error( '{}', err )
+ self.notifier.CloseUpdateProgress()
+ file.close()
+ except IOError as err:
+ self.logger.error( 'Error {} wile processing {}', err, destfile )
+ try:
+ self._update_end( full, 'ABORTED' )
+ self.notifier.CloseUpdateProgress()
+ file.close()
+ except Exception as err:
+ pass
+ return False
+
+ def GetNewestList( self, full ):
+ # get xz binary
+ xzbin = self._find_xz()
+ if xzbin is None:
+ self.notifier.ShowMissingXZError()
+ return False
+
+ ( url, compfile, destfile, avgrecsize ) = self._get_update_info( full )
+
+ # get mirrorlist
+ self.logger.info( 'Opening {}', url )
+ try:
+ data = urllib2.urlopen( url ).read()
+ except urllib2.URLError as err:
+ self.logger.error( 'Failure opening {}', url )
+ self.notifier.ShowDowloadError( url, err )
+ return False
+
+ root = etree.fromstring ( data )
+ urls = []
+ for server in root.findall( 'Server' ):
+ try:
+ URL = server.find( 'URL' ).text
+ Prio = server.find( 'Prio' ).text
+ urls.append( ( URL, Prio ) )
+ self.logger.info( 'Found mirror {} (Priority {})', URL, Prio )
+ except AttributeError as error:
+ pass
+ urls = sorted( urls, key = itemgetter( 1 ) )
+ urls = [ url[0] for url in urls ]
+ result = None
+
+ # cleanup downloads
+ self.logger.info( 'Cleaning up old downloads...' )
+ self._file_remove( compfile )
+ self._file_remove( destfile )
+
+ # download filmliste
+ self.logger.info( 'Trying to download file...' )
+ self.notifier.ShowDownloadProgress()
+ lasturl = ''
+ for url in urls:
+ try:
+ lasturl = url
+ self.notifier.UpdateDownloadProgress( 0, url )
+ result = urllib.urlretrieve( url, filename = compfile, reporthook = self._reporthook )
+ break
+ except IOError as err:
+ self.logger.error( 'Failure opening {}', url )
+ if result is None:
+ self.logger.info( 'No file downloaded' )
+ self.notifier.CloseDownloadProgress()
+ self.notifier.ShowDowloadError( lasturl, err )
+ return False
+
+ # decompress filmliste
+ self.logger.info( 'Trying to decompress file...' )
+ retval = subprocess.call( [ xzbin, '-d', compfile ] )
+ self.logger.info( 'Return {}', retval )
+ self.notifier.CloseDownloadProgress()
+ return retval == 0 and self._file_exists( destfile )
+
+ def _get_update_info( self, full ):
+ if full:
+ return (
+ FILMLISTE_AKT_URL,
+ os.path.join( self.settings.datapath, 'Filmliste-akt.xz' ),
+ os.path.join( self.settings.datapath, 'Filmliste-akt' ),
+ 600,
+ )
+ else:
+ return (
+ FILMLISTE_DIF_URL,
+ os.path.join( self.settings.datapath, 'Filmliste-diff.xz' ),
+ os.path.join( self.settings.datapath, 'Filmliste-diff' ),
+ 700,
+ )
+
+ def _find_xz( self ):
+ for xzbin in [ '/bin/xz', '/usr/bin/xz', '/usr/local/bin/xz' ]:
+ if self._file_exists( xzbin ):
+ return xzbin
+ if self.settings.updxzbin != '' and self._file_exists( self.settings.updxzbin ):
+ return self.settings.updxzbin
+ return None
+
+ def _file_exists( self, name ):
+ try:
+ s = os.stat( name )
+ return stat.S_ISREG( s.st_mode )
+ except OSError as err:
+ return False
+
+ def _file_size( self, name ):
+ try:
+ s = os.stat( name )
+ return s.st_size
+ except OSError as err:
+ return 0
+
+ def _file_remove( self, name ):
+ if self._file_exists( name ):
+ try:
+ os.remove( name )
+ return True
+ except OSError as err:
+ self.logger.error( 'Failed to remove {}: error {}', name, err )
+ return False
+
+ def _reporthook( self, blockcount, blocksize, totalsize ):
+ downloaded = blockcount * blocksize
+ if totalsize > 0:
+ percent = int( (downloaded * 100) / totalsize )
+ self.notifier.UpdateDownloadProgress( percent )
+ self.logger.debug( 'Downloading blockcount={}, blocksize={}, totalsize={}', blockcount, blocksize, totalsize )
+
+ def _update_start( self, full ):
+ self.logger.info( 'Initializing update...' )
+ self.add_chn = 0
+ self.add_shw = 0
+ self.add_mov = 0
+ self.add_chn = 0
+ self.add_shw = 0
+ self.add_mov = 0
+ self.del_chn = 0
+ self.del_shw = 0
+ self.del_mov = 0
+ self.index = 0
+ self.count = 0
+ self.film = {
+ "channel": "",
+ "show": "",
+ "title": "",
+ "aired": "1980-01-01 00:00:00",
+ "duration": "00:00:00",
+ "size": 0,
+ "description": "",
+ "website": "",
+ "url_sub": "",
+ "url_video": "",
+ "url_video_sd": "",
+ "url_video_hd": "",
+ "airedepoch": 0,
+ "geo": ""
+ }
+ return self.db.ftUpdateStart( full )
+
+ def _update_end( self, full, status ):
+ self.logger.info( 'Added: channels:%d, shows:%d, movies:%d ...' % ( self.add_chn, self.add_shw, self.add_mov ) )
+ ( self.del_chn, self.del_shw, self.del_mov, self.tot_chn, self.tot_shw, self.tot_mov ) = self.db.ftUpdateEnd( full and status == 'IDLE' )
+ self.logger.info( 'Deleted: channels:%d, shows:%d, movies:%d' % ( self.del_chn, self.del_shw, self.del_mov ) )
+ self.logger.info( 'Total: channels:%d, shows:%d, movies:%d' % ( self.tot_chn, self.tot_shw, self.tot_mov ) )
+ self.db.UpdateStatus(
+ status,
+ int( time.time() ) if status != 'ABORTED' else None,
+ None,
+ 1 if full else 0,
+ self.add_chn, self.add_shw, self.add_mov,
+ self.del_chn, self.del_shw, self.del_mov,
+ self.tot_chn, self.tot_shw, self.tot_mov
+ )
+
+ def _init_record( self ):
+ self.index = 0
+ self.film["title"] = ""
+ self.film["aired"] = "1980-01-01 00:00:00"
+ self.film["duration"] = "00:00:00"
+ self.film["size"] = 0
+ self.film["description"] = ""
+ self.film["website"] = ""
+ self.film["url_sub"] = ""
+ self.film["url_video"] = ""
+ self.film["url_video_sd"] = ""
+ self.film["url_video_hd"] = ""
+ self.film["airedepoch"] = 0
+ self.film["geo"] = ""
+
+ def _end_record( self, records ):
+ if self.count % 1000 == 0:
+ percent = int( self.count * 100 / records )
+ self.logger.info( 'In progress (%d%%): channels:%d, shows:%d, movies:%d ...' % ( percent, self.add_chn, self.add_shw, self.add_mov ) )
+ self.notifier.UpdateUpdateProgress( percent if percent <= 100 else 100, self.count, self.add_chn, self.add_shw, self.add_mov )
+ self.db.UpdateStatus(
+ add_chn = self.add_chn,
+ add_shw = self.add_shw,
+ add_mov = self.add_mov,
+ tot_chn = self.tot_chn + self.add_chn,
+ tot_shw = self.tot_shw + self.add_shw,
+ tot_mov = self.tot_mov + self.add_mov
+ )
+ self.count = self.count + 1
+ ( filmid, cnt_chn, cnt_shw, cnt_mov ) = self.db.ftInsertFilm( self.film, True )
+ else:
+ self.count = self.count + 1
+ ( filmid, cnt_chn, cnt_shw, cnt_mov ) = self.db.ftInsertFilm( self.film, False )
+ self.add_chn += cnt_chn
+ self.add_shw += cnt_shw
+ self.add_mov += cnt_mov
+
+ def _add_value( self, val ):
+ if self.index == 0:
+ if val != "":
+ self.film["channel"] = val
+ elif self.index == 1:
+ if val != "":
+ self.film["show"] = val[:255]
+ elif self.index == 2:
+ self.film["title"] = val[:255]
+ elif self.index == 3:
+ if len(val) == 10:
+ self.film["aired"] = val[6:] + '-' + val[3:5] + '-' + val[:2]
+ elif self.index == 4:
+ if ( self.film["aired"] != "1980-01-01 00:00:00" ) and ( len(val) == 8 ):
+ self.film["aired"] = self.film["aired"] + " " + val
+ elif self.index == 5:
+ if len(val) == 8:
+ self.film["duration"] = val
+ elif self.index == 6:
+ if val != "":
+ self.film["size"] = int(val)
+ elif self.index == 7:
+ self.film["description"] = val
+ elif self.index == 8:
+ self.film["url_video"] = val
+ elif self.index == 9:
+ self.film["website"] = val
+ elif self.index == 10:
+ self.film["url_sub"] = val
+ elif self.index == 12:
+ self.film["url_video_sd"] = self._make_url(val)
+ elif self.index == 14:
+ self.film["url_video_hd"] = self._make_url(val)
+ elif self.index == 16:
+ if val != "":
+ self.film["airedepoch"] = int(val)
+ elif self.index == 18:
+ self.film["geo"] = val
+ self.index = self.index + 1
+
+ def _make_search( self, val ):
+ cset = string.letters + string.digits + ' _-#'
+ search = ''.join( [ c for c in val if c in cset ] )
+ return search.upper().strip()
+
+ def _make_url( self, val ):
+ x = val.split( '|' )
+ if len( x ) == 2:
+ cnt = int( x[0] )
+ return self.film["url_video"][:cnt] + x[1]
+ else:
+ return val
diff --git a/plugin.video.mediathekview/de/__init__.py b/plugin.video.mediathekview/de/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.mediathekview/de/__init__.py
diff --git a/plugin.video.mediathekview/de/yeasoft/__init__.py b/plugin.video.mediathekview/de/yeasoft/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.mediathekview/de/yeasoft/__init__.py
diff --git a/plugin.video.mediathekview/de/yeasoft/base/Logger.py b/plugin.video.mediathekview/de/yeasoft/base/Logger.py
new file mode 100644
index 0000000..7fad5fc
--- /dev/null
+++ b/plugin.video.mediathekview/de/yeasoft/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/de/yeasoft/base/__init__.py b/plugin.video.mediathekview/de/yeasoft/base/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.mediathekview/de/yeasoft/base/__init__.py
diff --git a/plugin.video.mediathekview/de/yeasoft/kodi/KodiAddon.py b/plugin.video.mediathekview/de/yeasoft/kodi/KodiAddon.py
new file mode 100644
index 0000000..9d2fa04
--- /dev/null
+++ b/plugin.video.mediathekview/de/yeasoft/kodi/KodiAddon.py
@@ -0,0 +1,68 @@
+# -*- 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
new file mode 100644
index 0000000..f656b31
--- /dev/null
+++ b/plugin.video.mediathekview/de/yeasoft/kodi/KodiLogger.py
@@ -0,0 +1,55 @@
+# -*- 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
new file mode 100644
index 0000000..dee8dbb
--- /dev/null
+++ b/plugin.video.mediathekview/de/yeasoft/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/de/yeasoft/kodi/__init__.py b/plugin.video.mediathekview/de/yeasoft/kodi/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugin.video.mediathekview/de/yeasoft/kodi/__init__.py
diff --git a/plugin.video.mediathekview/mvupdate b/plugin.video.mediathekview/mvupdate
new file mode 100755
index 0000000..56aedc0
--- /dev/null
+++ b/plugin.video.mediathekview/mvupdate
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# MIT License
+#
+# Copyright (c) 2017-2018, Leo Moll
+#
+# 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.
+
+# -- Imports ------------------------------------------------
+from __future__ import unicode_literals
+from classes.mvupdate import UpdateApp
+
+# -- Main Code ----------------------------------------------
+if __name__ == '__main__':
+ app = UpdateApp()
+ app.Init()
+ app.Run()
+ app.Exit()
+ del app
diff --git a/plugin.video.mediathekview/resources/fanart.jpg b/plugin.video.mediathekview/resources/fanart.jpg
new file mode 100644
index 0000000..4d9bd9d
--- /dev/null
+++ b/plugin.video.mediathekview/resources/fanart.jpg
Binary files differ
diff --git a/plugin.video.mediathekview/resources/icon.jpg b/plugin.video.mediathekview/resources/icon.jpg
new file mode 100644
index 0000000..79073be
--- /dev/null
+++ b/plugin.video.mediathekview/resources/icon.jpg
Binary files differ
diff --git a/plugin.video.mediathekview/resources/language/English/strings.po b/plugin.video.mediathekview/resources/language/English/strings.po
new file mode 100644
index 0000000..e4c3cf5
--- /dev/null
+++ b/plugin.video.mediathekview/resources/language/English/strings.po
@@ -0,0 +1,300 @@
+# 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
new file mode 100644
index 0000000..c6da721
--- /dev/null
+++ b/plugin.video.mediathekview/resources/language/German/strings.po
@@ -0,0 +1,302 @@
+# 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
new file mode 100644
index 0000000..aadc963
--- /dev/null
+++ b/plugin.video.mediathekview/resources/language/Italian/strings.po
@@ -0,0 +1,303 @@
+# 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/screenshot1.png b/plugin.video.mediathekview/resources/screenshot1.png
new file mode 100644
index 0000000..cfee38d
--- /dev/null
+++ b/plugin.video.mediathekview/resources/screenshot1.png
Binary files differ
diff --git a/plugin.video.mediathekview/resources/screenshot2.png b/plugin.video.mediathekview/resources/screenshot2.png
new file mode 100644
index 0000000..58c7d23
--- /dev/null
+++ b/plugin.video.mediathekview/resources/screenshot2.png
Binary files differ
diff --git a/plugin.video.mediathekview/resources/screenshot3.png b/plugin.video.mediathekview/resources/screenshot3.png
new file mode 100644
index 0000000..edec0c6
--- /dev/null
+++ b/plugin.video.mediathekview/resources/screenshot3.png
Binary files differ
diff --git a/plugin.video.mediathekview/resources/screenshot4.png b/plugin.video.mediathekview/resources/screenshot4.png
new file mode 100644
index 0000000..7f41e8b
--- /dev/null
+++ b/plugin.video.mediathekview/resources/screenshot4.png
Binary files differ
diff --git a/plugin.video.mediathekview/resources/screenshot5.png b/plugin.video.mediathekview/resources/screenshot5.png
new file mode 100644
index 0000000..9e82c02
--- /dev/null
+++ b/plugin.video.mediathekview/resources/screenshot5.png
Binary files differ
diff --git a/plugin.video.mediathekview/resources/settings.xml b/plugin.video.mediathekview/resources/settings.xml
new file mode 100644
index 0000000..b50510a
--- /dev/null
+++ b/plugin.video.mediathekview/resources/settings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<settings>
+ <category label="30001">
+ <setting id="firstrun" type="bool" default="true" visible="false" />
+ <setting id="quality" type="bool" label="30110" default="true" />
+ <setting id="nofuture" type="bool" label="30111" default="true" />
+ <setting id="minlength" type="slider" label="30112" default="0" range="0,30" />
+ <setting id="groupshows" type="bool" label="30113" default="true" />
+ <setting id="downloadpath" type="folder" label="30114" source="auto" option="writeable" />
+ </category>
+ <category label="30002">
+ <setting id="dbtype" type="enum" label="30210" default="0" lvalues="30221|30222" />
+ <setting id="dbhost" type="text" label="30211" default="localhost" visible="eq(-1,1)" />
+ <setting id="dbuser" type="text" label="30212" default="filmliste" visible="eq(-2,1)" />
+ <setting id="dbpass" type="text" label="30213" default="" option="hidden" visible="eq(-3,1)" />
+ <setting id="dbdata" type="text" label="30214" default="filmliste" visible="eq(-4,1)" />
+ <setting id="updenabled" type="bool" label="30231" default="true" />
+ <setting id="updinterval" type="slider" label="30232" default="2" range="1,24" visible="eq(-1,true)" />
+ <setting id="updxzbin" type="file" label="30233" default="" visible="eq(-2,true)" />
+
+ <!-- setting type="action" label="30238" action="RunScript(plugin.video.mediathekview, downloadreport)"/ -->
+ </category>
+</settings>
diff --git a/plugin.video.mediathekview/resources/sql/exportstruct.sh b/plugin.video.mediathekview/resources/sql/exportstruct.sh
new file mode 100755
index 0000000..04c18a6
--- /dev/null
+++ b/plugin.video.mediathekview/resources/sql/exportstruct.sh
@@ -0,0 +1,3 @@
+#!/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
new file mode 100644
index 0000000..341fb74
--- /dev/null
+++ b/plugin.video.mediathekview/resources/sql/filmliste-mysql-v1.sql
@@ -0,0 +1,454 @@
+-- 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
new file mode 100644
index 0000000..8e7ee09
--- /dev/null
+++ b/plugin.video.mediathekview/resources/sql/filmliste-sqlite-v1.sql
@@ -0,0 +1,104 @@
+/*
+ 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
new file mode 100644
index 0000000..a299ee4
--- /dev/null
+++ b/plugin.video.mediathekview/service.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+#
+# MIT License
+#
+# Copyright (c) 2017-2018, Leo Moll
+#
+# 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.
+
+# -- Imports ------------------------------------------------
+from __future__ import unicode_literals # ,absolute_import, division
+
+import time
+import xbmc
+
+from de.yeasoft.kodi.KodiAddon import KodiService
+
+from classes.store import Store
+from classes.notifier import Notifier
+from classes.settings import Settings
+from classes.updater import MediathekViewUpdater
+
+# -- Classes ------------------------------------------------
+class MediathekViewMonitor( xbmc.Monitor ):
+ def __init__( self, service ):
+ super( MediathekViewMonitor, self ).__init__()
+ self.service = service
+ self.logger = service.getNewLogger( 'Monitor' )
+ self.logger.info( 'Startup' )
+
+ def __del__( self ):
+ self.logger.info( 'Shutdown' )
+
+ def onSettingsChanged( self ):
+ self.service.ReloadSettings()
+
+class MediathekViewService( KodiService ):
+ def __init__( self ):
+ super( MediathekViewService, self ).__init__()
+ self.setTopic( 'Service' )
+ 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
+
+ def Init( self ):
+ self.info( 'Startup' )
+ self.updater.Init()
+
+ def Run( self ):
+ self.info( 'Starting up...' )
+ while not self.monitor.abortRequested():
+ 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 )
+ # Sleep/wait for abort for 60 seconds
+ if self.monitor.waitForAbort( 60 ):
+ # Abort was requested while waiting. We should exit
+ break
+ self.info( 'Exiting...' )
+
+ def Exit( self ):
+ self.info( 'Shutdown' )
+ self.updater.Exit()
+
+ def ReloadSettings( self ):
+ # TODO: support online reconfiguration
+ pass
+
+# -- Main Code ----------------------------------------------
+if __name__ == '__main__':
+ service = MediathekViewService()
+ service.Init()
+ service.Run()
+ service.Exit()
+ del service