diff options
author | Leo Moll <leo.moll@dtms.de> | 2018-01-11 12:16:45 +0100 |
---|---|---|
committer | Martijn Kaijser <martijn@xbmc.org> | 2018-01-15 18:48:50 +0100 |
commit | e5d0f50b8d39ab7fd745b8f384ec622f6b0df5d9 (patch) | |
tree | 330896b0ec477109bcf87798feb7076a65588c45 | |
parent | 7614cf25cf45728579713777796a41bed914fe05 (diff) |
[plugin.video.mediathekview] 0.3.4
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 Binary files differnew file mode 100644 index 0000000..4d9bd9d --- /dev/null +++ b/plugin.video.mediathekview/resources/fanart.jpg diff --git a/plugin.video.mediathekview/resources/icon.jpg b/plugin.video.mediathekview/resources/icon.jpg Binary files differnew file mode 100644 index 0000000..79073be --- /dev/null +++ b/plugin.video.mediathekview/resources/icon.jpg 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 Binary files differnew file mode 100644 index 0000000..cfee38d --- /dev/null +++ b/plugin.video.mediathekview/resources/screenshot1.png diff --git a/plugin.video.mediathekview/resources/screenshot2.png b/plugin.video.mediathekview/resources/screenshot2.png Binary files differnew file mode 100644 index 0000000..58c7d23 --- /dev/null +++ b/plugin.video.mediathekview/resources/screenshot2.png diff --git a/plugin.video.mediathekview/resources/screenshot3.png b/plugin.video.mediathekview/resources/screenshot3.png Binary files differnew file mode 100644 index 0000000..edec0c6 --- /dev/null +++ b/plugin.video.mediathekview/resources/screenshot3.png diff --git a/plugin.video.mediathekview/resources/screenshot4.png b/plugin.video.mediathekview/resources/screenshot4.png Binary files differnew file mode 100644 index 0000000..7f41e8b --- /dev/null +++ b/plugin.video.mediathekview/resources/screenshot4.png diff --git a/plugin.video.mediathekview/resources/screenshot5.png b/plugin.video.mediathekview/resources/screenshot5.png Binary files differnew file mode 100644 index 0000000..9e82c02 --- /dev/null +++ b/plugin.video.mediathekview/resources/screenshot5.png 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 |