diff --git a/seriesplugin/CONTROL/conffiles b/seriesplugin/CONTROL/conffiles new file mode 100644 index 000000000..0b3466b7b --- /dev/null +++ b/seriesplugin/CONTROL/conffiles @@ -0,0 +1,2 @@ +/etc/enigma2/seriesplugin_patterns.json +/etc/enigma2/seriesplugin_pattern_directories.json diff --git a/seriesplugin/CONTROL/control b/seriesplugin/CONTROL/control new file mode 100644 index 000000000..ca908254b --- /dev/null +++ b/seriesplugin/CONTROL/control @@ -0,0 +1,6 @@ +Package: enigma2-plugin-extensions-seriesplugin +Description: Find and rename series +Maintainer: betonme +Architecture: all +Homepage: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=168016 +Depends: enigma2, python-difflib, python-json, python-re, python-xml, python-xmlrpc diff --git a/seriesplugin/CONTROL/postinst b/seriesplugin/CONTROL/postinst new file mode 100644 index 000000000..8dc84c2aa --- /dev/null +++ b/seriesplugin/CONTROL/postinst @@ -0,0 +1,9 @@ +#!/bin/sh +echo "********************************************************" +echo "* SeriesPlugin installed *" +echo "* Coded by betonme (c) 2012 *" +echo "* Support: IHAD *" +echo "* *" +echo "* Restart Enigma-2 GUI to activate the plugin *" +echo "********************************************************" +exit 0 diff --git a/seriesplugin/CONTROL/postrm b/seriesplugin/CONTROL/postrm new file mode 100644 index 000000000..69c19f2e1 --- /dev/null +++ b/seriesplugin/CONTROL/postrm @@ -0,0 +1,4 @@ +#!/bin/sh +rm -rf /usr/lib/enigma2/python/Plugins/Extensions/SeriesPlugin/ +echo "Plugin removed! You should restart enigma2 now!" +exit 0 diff --git a/seriesplugin/Makefile.am b/seriesplugin/Makefile.am new file mode 100644 index 000000000..dd21290dc --- /dev/null +++ b/seriesplugin/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = src po etc meta \ No newline at end of file diff --git a/seriesplugin/etc/Makefile.am b/seriesplugin/etc/Makefile.am new file mode 100644 index 000000000..7b3c31ffe --- /dev/null +++ b/seriesplugin/etc/Makefile.am @@ -0,0 +1,5 @@ +installdir = $(sysconfdir)/enigma2 + +# Do not install the files +# These are just samples for user specific patterns +#install_DATA = seriesplugin_patterns.json seriesplugin_pattern_directories.json diff --git a/seriesplugin/etc/seriesplugin_pattern_directories.json b/seriesplugin/etc/seriesplugin_pattern_directories.json new file mode 100644 index 000000000..4998603c7 --- /dev/null +++ b/seriesplugin/etc/seriesplugin_pattern_directories.json @@ -0,0 +1,29 @@ +[ + [ + [ " SeriesPlugin " ], + [ " List of directory patterns in JSON notation " ], + [ " String printf pattern , Setup entry " ] + ], + [ + ["Off" , "Disabled"], + + ["{org:s}/{series:s}/{season:02d}/" , "Original/Series/01/"], + ["{org:s}/{series:s}/S{season:02d}/" , "Original/Series/S01/"], + ["{org:s}/{series:s}/{rawseason:s}/" , "Original/Series/Raw/"], + + ["{org:s}/{series:s}/Season {season:02d}/" , "Original/Series/Season 01/"], + ["{org:s}/{series:s}/Season {rawseason:s}/" , "Original/Series/Season Raw/"], + + ["{org:s}/{series:s} {season:02d}/" , "Original/Series 01/"], + ["{org:s}/{series:s} S{season:02d}/" , "Original/Series S01/"], + + ["{org:s}/{series:s} Season {season:02d}/" , "Original/Series Season 01/"], + ["{org:s}/{series:s} Season {rawseason:s}/" , "Original/Series Season Raw/"], + + ["{org:s}/{service:s}/{series:s} Season {rawseason:s}/" , "Original/Service/Series Season Raw/"], + ["{org:s}/{channel:s}/{series:s} Season {rawseason:s}/" , "Original/Channel/Series Season Raw/"], + + ["{org:s}/{date:s}/{series:s}/" , "Date/Series/"], + ["{org:s}/{time:s}/{series:s}/" , "Time/Series/"] + ] +] \ No newline at end of file diff --git a/seriesplugin/etc/seriesplugin_patterns.json b/seriesplugin/etc/seriesplugin_patterns.json new file mode 100644 index 000000000..c40123a2f --- /dev/null +++ b/seriesplugin/etc/seriesplugin_patterns.json @@ -0,0 +1,78 @@ +[ + [ + [ " SeriesPlugin " ], + [ " List of episode patterns in JSON notation " ], + [ " String printf pattern , Setup entry " ] + ], + [ + ["Off" , "Disabled"], + + ["{org:s} S{season:02d}E{episode:02d}" , "Org S01E01"], + ["{org:s} S{season:d}E{episode:d}" , "Org S1E1"], + + ["{org:s} S{season:02d}E{episode:02d} {title:s}" , "Org S01E01 Title"], + ["{org:s} S{season:d}E{episode:d} {title:s}" , "Org S1E1 Title"], + + ["{org:s} {title:s} S{season:02d}E{episode:02d}" , "Org Title S01E01"], + ["{org:s} {title:s} S{season:d}E{episode:d}" , "Org Title S1E1"], + + ["{org:s} - S{season:02d}E{episode:02d} - {title:s}" , "Org - S01E01 - Title"], + ["{org:s} - S{season:2d}E{episode:2d} - {title:s}" , "Org - S1E1 - Title"], + + ["S{season:02d}E{episode:02d}" , "S01E01"], + ["S{season:02d}E{episode:02d} {org:s}" , "S01E01 Org"], + ["S{season:d}E{episode:d} {org:s}" , "S1E1 Org"], + + ["S{season:02d}E{episode:02d} {title:s} {org:s}" , "S01E01 Title Org"], + ["S{season:d}E{episode:d} {title:s} {org:s}" , "S1E1 Title Org"], + + ["{title:s} S{season:02d}E{episode:02d} {org:s}" , "Title S01E01 Org"], + ["{title:s} S{season:d}E{episode:d} {org:s}" , "Title S1E1 Org"], + + ["{title:s}" , "Title"], + ["{title:s} {org:s}" , "Title Org"], + ["{title:s} {series:s}" , "Title Series"], + + ["{org:s} {title:s}" , "Org Title"], + ["{series:s} {title:s}" , "Series Title"], + + ["{series:s} S{season:02d}E{episode:02d}" , "Series S01E01"], + ["{series:s} S{season:d}E{episode:d}" , "Series S1E1"], + + ["{series:s} S{season:02d}E{episode:02d} {title:s}" , "Series S01E01 Title"], + ["{series:s} S{season:d}E{episode:d} {title:s}" , "Series S1E1 Title"], + + ["{series:s} {title:s} S{season:02d}E{episode:02d}" , "Series Title S01E01"], + ["{series:s} {title:s} S{season:d}E{episode:d}" , "Series Title S1E1"], + + ["S{season:02d}E{episode:02d} {series:s}" , "S01E01 Series"], + ["S{season:d}E{episode:d} {series:s}" , "S1E1 Series"], + + ["S{season:02d}E{episode:02d} {title:s} {series:s}" , "S01E01 Title Series"], + ["S{season:d}E{episode:d} {title:s} {series:s}" , "S1E1 Title Series"], + + ["{title:s} S{season:02d}E{episode:02d} {series:s}" , "Title S01E01 Series"], + ["{title:s} S{season:d}E{episode:d} {series:s}" , "Title S1E1 Series"], + + ["{series:s} - s{season:02d}e{episode:02d} - {title:s}" , "Series - s01e01 - Title"], + ["{series:s} - S{season:02d}E{episode:02d} - {title:s}" , "Series - S01E01 - Title"], + + ["{org:s}_S{season:02d}EP{episode:02d}" , "Org_S01EP01"], + ["{org:s}_S{season:02d}EP{episode:02d_}" , "Org_S01EP01_"], + + ["{series:s} S{rawseason:s} E{rawepisode:s} {title:s}" , "Series SRaw ERaw Title"], + ["{series:s} S{rawseason:s}E{rawepisode:s} {title:s}" , "Series SRawERaw Title"], + ["{series:s} {rawseason:s} {rawepisode:s} {title:s}" , "Series Raw Raw Title"], + ["{series:s} {rawseason:s}{rawepisode:s} {title:s}" , "Series RawRaw Title"], + + ["{series:s} S{season:02d} E{rawepisode:s} {title:s}" , "Series S01 ERaw Title"], + ["{series:s} S{season:02d}E{rawepisode:s} {title:s}" , "Series S01ERaw Title"], + ["{series:s} - S{season:02d}E{rawepisode:s} - {title:s}" , "Series - S01ERaw - Title"], + + ["{channel:s} {series:s} S{season:02d} E{rawepisode:s} {title:s}" , "Channel Series S01 ERaw Title"], + ["{service:s} {series:s} S{season:02d}E{rawepisode:s} {title:s}" , "Service Series S01ERaw Title"], + + ["{date:s} {channel:s} {series:s} S{season:02d} E{rawepisode:s} {title:s}" , "Date Channel Series S01 ERaw Title"], + ["{date:s} {time:s} {channel:s} {series:s} S{season:02d} E{rawepisode:s} {title:s}" , "Date Time Channel Series S01 ERaw Title"] + ] +] \ No newline at end of file diff --git a/seriesplugin/meta/Makefile.am b/seriesplugin/meta/Makefile.am new file mode 100644 index 000000000..ce573a3a9 --- /dev/null +++ b/seriesplugin/meta/Makefile.am @@ -0,0 +1,5 @@ +installdir = $(datadir)/meta/ + +dist_install_DATA = plugin_seriesplugin.xml + +#EXTRA_DIST = seriesplugin.jpg \ No newline at end of file diff --git a/seriesplugin/meta/plugin_seriesplugin.xml b/seriesplugin/meta/plugin_seriesplugin.xml new file mode 100644 index 000000000..7104aa691 --- /dev/null +++ b/seriesplugin/meta/plugin_seriesplugin.xml @@ -0,0 +1,18 @@ + + + + + + + betonme + SeriesPlugin + enigma2-plugin-extensions-seriesplugin + Add season and episode information to Your recordings. + Automatically search and add season and episode information to Your timer and recordings. + + + + + + + diff --git a/seriesplugin/po/Makefile.am b/seriesplugin/po/Makefile.am new file mode 100644 index 000000000..f100590ff --- /dev/null +++ b/seriesplugin/po/Makefile.am @@ -0,0 +1,3 @@ +PLUGIN = SeriesPlugin +LANGS = de +include $(top_srcdir)/Rules-po.mak diff --git a/seriesplugin/po/de.po b/seriesplugin/po/de.po new file mode 100644 index 000000000..c1960822a --- /dev/null +++ b/seriesplugin/po/de.po @@ -0,0 +1,512 @@ +msgid "" +msgstr "" +"Project-Id-Version: SeriesPlugin\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-03-29 12:38+0200\n" +"PO-Revision-Date: 2016-03-29 12:46+0200\n" +"Last-Translator: James Blond \n" +"Language-Team: \n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.6\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-Basepath: .\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Channel Editor" +msgstr "Kanal Editor" + +msgid "Cancel" +msgstr "Beenden" + +msgid "OK" +msgstr "OK" + +msgid "Remove" +msgstr "Entfernen" + +msgid "Auto match" +msgstr "Automatisch zuweisen" + +msgid "Show popup to add Stb Channel" +msgstr "Zeige Popup wenn STB Kanal hinzugefügt" + +msgid "Cancel and close" +msgstr "Abbrechen und schließen" + +msgid "Reset channels" +msgstr "Kanäle zurücksetzen" + +msgid "Previeous page" +msgstr "Vorherige Seite" + +msgid "Next page" +msgstr "Nächste Seite" + +msgid "One row up" +msgstr "Eine Zeile nach oben" + +msgid "One row down" +msgstr "Eine Zeile nach unten" + +msgid "Next bouquet" +msgstr "Nächstes Bouquet" + +msgid "Previous bouquet" +msgstr "Vorheriges Bouquet" + +msgid "Save and close" +msgstr "Speichern und schließen" + +msgid "Remove channel" +msgstr "Kanal entfernen" + +msgid "" +"If You are using SD and HD channels in parallel, You have to match both " +"channels separately!" +msgstr "" +"Wenn Du SD- und HD-Kanäle parallel verwendest, müssen Sie separat zugewiesen " +"werden!" + +msgid "Load STB channels for bouquet" +msgstr "Lade STB Kanäle für Bouquet" + +msgid "Load Web channels for bouquet" +msgstr "Lade Web Kanäle für Bouquet" + +msgid "Problem during loading Webchannels" +msgstr "Problem beim laden der Web-Kanäle" + +msgid "STB- / Web-Channel for bouquet:" +msgstr "STB- / Web-Kanal für Bouquet:" + +msgid "Error check log file" +msgstr "Fehler überprüfe Protokoll Datei" + +msgid "Channel matching..." +msgstr "Kanalübereinstimmung..." + +msgid "Add Web Channel" +msgstr "Web Kanal zufügen" + +msgid "Channel '- %(servicename)s - %(remote)s -' added." +msgstr "Kanal '- %(servicename)s - %(remote)s -' hinzugefügt." + +msgid "Add channel (Yes) or replace it (No)" +msgstr "Kanal anfügen (Ja) oder entfernen (Nein)" + +msgid "Channel '- %(servicename)s - %(remote)s -' replaced." +msgstr "Kanal '- %(servicename)s - %(remote)s -' entfernt." + +msgid "Remove '%s'?" +msgstr "Entfernen '%s'?" + +msgid "Channel '- %s -' removed." +msgstr "Kanal '- %s -' entfernt." + +msgid "Reset channel list?" +msgstr "Kanalliste zurücksetzen?" + +msgid "Skipping old channels file" +msgstr "Überspringe alte Kanal Datei" + +msgid "Don't edit this manually unless you really know what you are doing" +msgstr "Bitte nicht bearbeiten wenn Du nicht wirklich weißt was du tust" + +msgid "Your pattern file is corrupt" +msgstr "Vorlage Datei ist beschädigt" + +msgid "Skipping lookup because no show name is specified" +msgstr "Übersprunge wenn kein Sendungsnamen angegeben" + +msgid "Skipping lookup because no begin timestamp is specified" +msgstr "Übersprunge wenn keine Anfangszeit angegeben" + +msgid "Skipping lookup because no channel is specified" +msgstr "Übersprunge wenn kein Kanal angegeben" + +msgid "No matching channel found." +msgstr "Kein passender Kanal gefunden" + +msgid "Please open the Channel Editor and add the channel manually." +msgstr "Bitte öffne den Kanaleditor und füge den Kanal manuell hinzu." + +msgid "No match found" +msgstr "Keine Übereinstimmung gefunden" + +msgid "Your autotimer is deprecated" +msgstr "Deine Autotimer Version ist veraltet" + +msgid "Please update it" +msgstr "Bitte updaten" + +msgid "Error missing dependency" +msgstr "Fehler fehlende Abhängigkeit" + +msgid "Please install missing python paket manually" +msgstr "Bitte installiere das fehlende Python Paket manuell" + +msgid "Skip check during running records" +msgstr "Überspringe Prüfung bei laufender Aufnahme" + +msgid "Can be configured within the setup" +msgstr "Kann in Einstellungen konfiguriert werden" + +msgid "Skip check because of pattern match" +msgstr "Überspringe Prüfung wenn Vorlage übereinstimmt" + +msgid "No identifier available" +msgstr "Keine Kennung verfügbar" + +msgid "Please check Your installation" +msgstr "Bitte überprüfe deine Installation" + +msgid "Channels are not matched" +msgstr "Keine Kanalübereinstimmung" + +msgid "Please open the channel editor (setup) and match them" +msgstr "Bitte öffne den Kanaleditor (Einstellungen) und weise ihn zu" + +msgid "SeriesPlugin is deactivated" +msgstr "SeriesPlugin ist ausgeschaltet" + +msgid "Failed: %s." +msgstr "Gescheitert: %s." + +msgid "No data available" +msgstr "Keine Daten vorhanden" + +msgid "Finished with errors:\n" +msgstr "Erledigt mit Fehlern:\n" + +msgid "Lookup of %d episodes was successful" +msgstr "Nachschlagen von %d Episoden war erfolgreich" + +msgid "Configuration" +msgstr "Konfiguration" + +msgid "Show Log" +msgstr "Protokoll anzeigen" + +msgid "Channel Edit" +msgstr "Kanal bearbeiten" + +msgid "Enable SeriesPlugin" +msgstr "SeriesPlugin aktivieren" + +msgid "Enable support for EPGImport" +msgstr "EPGImport Unterstützung aktivieren" + +msgid "Enable support for XMLTVImport" +msgstr "XMLTVImport Unterstützung aktivieren" + +msgid "Show in info menu" +msgstr "Serien-Info im Info Menü anzeigen" + +msgid "Show in extensions menu" +msgstr "Serien-Info im Erweiterten Menü anzeigen" + +msgid "Show in epg menu" +msgstr "Serien-Info im EPG-Menü anzeigen" + +msgid "Show in channel menu" +msgstr "Serien-Info in Kanalauswahl anzeigen" + +msgid "Show Info in movie list menu" +msgstr "Serien-Info im Filmlisten Menü anzeigen" + +msgid "Show Rename in movie list menu" +msgstr "Umbenennen im Filmlisten Menü anzeigen" + +msgid "Check timer list from extension menu" +msgstr "Timerliste aus dem Erweiterten Menü prüfen" + +msgid "Record title episode pattern" +msgstr "Episoden Vorlage für den Aufnahmetitel" + +msgid "Record description episode pattern" +msgstr "Episoden Vorlage für Aufnahmebeschreibung" + +msgid "Composition of the recording filenames" +msgstr "Dateinamen-Zusammensetzung der Aufnahmen" + +msgid "Record directory pattern" +msgstr "Episoden Vorlage Aufnahmeverzeichnis" + +msgid "Default season" +msgstr "Voreingestellte Staffel" + +msgid "Default episode" +msgstr "Voreingestellte Folge" + +msgid "Replace special characters in title" +msgstr "Sonderzeichen im Titel ersetzen" + +msgid "Main bouquet for channel editor" +msgstr "Haupt-Bouquet für Kanal-Editor" + +msgid "Rename files" +msgstr "Datei(en) umbenennen" + +msgid "Use legacy filenames" +msgstr "Verwende ältere Dateinamen" + +msgid "Append '_' if file exist" +msgstr "Anhängen '_' wenn die Datei vorhanden ist" + +msgid "Max time drift to match episode" +msgstr "Maximale Zeitabweichung um Folge(n) zu erkennen" + +msgid "Title search depths" +msgstr "Titel Suchtiefe" + +msgid "Skip search if pattern matches" +msgstr "Überspringe Suche wenn Vorlage übereinstimmt" + +msgid "Skip search during records" +msgstr "Suchen während einer Aufnahme deaktivieren" + +msgid "AutoTimer independent mode" +msgstr "Serien Infos suchen ohne AutoTimer (unabhängiger Modus)" + +msgid "Check timer every x minutes" +msgstr "Timer alle (X) Minuten prüfen" + +msgid "Check Timer for corresponding EPG events" +msgstr "Prüfe Timer auf entsprechenden EPG Eintrag" + +msgid "Add tag 'SeriesPlugin' to timer" +msgstr "Füge TAG 'SeriesPlugin' zu Timer hinzu" + +msgid "Socket timeout" +msgstr "Zeitlimt für Anfragen zum Proxy (Sekunden)" + +msgid "Timeout for Success Popups" +msgstr "Anzeigedauer für erfolgreiche Popups" + +msgid "Timeout for Warnings Popups" +msgstr "Anzeigedauer Warnungen (Popup)" + +msgid "Channel matching file" +msgstr "Passende Kanal Datei" + +msgid "Episode pattern file" +msgstr "Episoden Vorlage Datei" + +msgid "Directory pattern file" +msgstr "Episoden Vorlage Verzeichnis" + +msgid "Poll automatically" +msgstr "Poll automatisch" + +msgid "Startup delay (in min)" +msgstr "Startverzögerung (in Minuten)" + +msgid "Poll Interval (in h)" +msgstr "Poll Abfrage (in Stunden)" + +msgid "Timeout (in min)" +msgstr "Zeitlimit (in Minuten)" + +msgid "Debug: Print debug messages (Shell)" +msgstr "Debug: Schreibe Debug Nachrichten (Shell)" + +msgid "Debug: Write Log" +msgstr "Debug: Protokolldatei erstellen (Log)" + +msgid "Debug: Log file path" +msgstr "Debug: Protokolldatei Pfad" + +msgid "Send debug messages to shell" +msgstr "Sende Debug Nachrichten (Shell)" + +msgid "Write debug messages into file" +msgstr "Schreibe Fehlerprotokoll in Datei (Log)" + +msgid "Location and name of log file" +msgstr "Ort und Name der Protokolldatei" + +msgid "Enable recording debug (Timer log)" +msgstr "Aktiviere Aufnahmeprotokoll (Timer Protokoll)" + +msgid "Really close without saving settings?" +msgstr "Wirklich schließen ohne die Einstellungen zu speichern?" + +msgid "Independent mode exception" +msgstr "Ausnahme Timer Unabhängiger Modus" + +msgid "SeriesPlugin Info" +msgstr "SeriesPlugin Information" + +msgid "Retrieving Season, Episode and Title..." +msgstr "Lade... Infos zu Staffel, Folge und Titel..." + +msgid "{title:s}" +msgstr "{title:s}" + +msgid "" +"Episode: {rawepisode:s}\n" +"{title:s}" +msgstr "" +"Folge: {rawepisode:s}\n" +"{title:s}" + +msgid "" +"Season: {rawseason:s}\n" +"{title:s}" +msgstr "" +"Staffel: {rawseason:s}\n" +"{title:s}" + +msgid "" +"Season: {rawseason:s} Episode: {rawepisode:s}\n" +"{title:s}" +msgstr "" +"Staffel: {rawseason:s} Folge: {rawepisode:s}\n" +"{title:s}" + +msgid "No matching episode found" +msgstr "Kein Treffer für Folge gefunden" + +msgid "%d min" +msgstr "%d Minuten" + +msgid "Rename" +msgstr "Umbenennen" + +msgid "Record" +msgstr "Aufnahme" + +msgid "Successfully renamed" +msgstr "Erfolgreich umbenannt" + +msgid "Renaming failed" +msgstr "Umbenennen gescheitert" + +msgid "Do you really want to delete %s?" +msgstr "Möchtest Du wirklich %s löschen?" + +msgid "Skipping rename because file already exists" +msgstr "Überspringe Umbenennen wenn Datei bereits vorhanden ist" + +msgid "Do You want to start renaming?" +msgstr "Möchtest Du das Umbenennen beginnen?" + +msgid "Record rename has been finished with %d errors:\n" +msgstr "Aufnahme umbenennen wurde beendet mit %d Fehlern:\n" + +msgid "%d records renamed successfully" +msgstr "%d Aufnahmen erfolgreich umbenannt" + +msgid "Skipping timer because it is already in queue" +msgstr "Überspringe Timer wenn in Warteschleife vorhanden" + +msgid "Skipping timer because it is already handled" +msgstr "Überspringe Timer wenn in Bearbeitung" + +msgid "Skipping timer because it starts in less than 60 seconds" +msgstr "Überspringe Timer wenn er in weniger als 60 Sekunden startet" + +msgid "Skipping timer because it is already running" +msgstr "Überspringe Timer wenn er bereits ausgeführt wird" + +msgid "Skipping timer because it is a just play timer" +msgstr "Überspringe Timer wenn es ein Wiedergabetimer ist" + +msgid "Skipping timer because it is already modified %s" +msgstr "Überspringe Timer wenn %s bereits geändert sind" + +msgid "Skipping timer because no event was found" +msgstr "Überspringe Timer wenn kein Ereignis gefunden wurde" + +msgid "Try to find infos for %s" +msgstr "Versuche Informationen zu finden für %s" + +msgid "Success: %s" +msgstr "Erfolgreich: %s" + +msgid "Timer rename has been finished with %d errors:\n" +msgstr "Timer umbenennen wurde beendet mit %d Fehlern:\n" + +msgid "%d timer renamed successfully" +msgstr "%d Timer erfolgreich umbenannt" + +msgid "Show Log file" +msgstr "Protokolldatei anzeigen (Log)" + +msgid "Reading log file...\n" +msgstr "Lese Protokolldatei... \n" + +msgid "" +"\n" +"Cancel?" +msgstr "" +"\n" +"Abbruch?" + +msgid "No log file found" +msgstr "Keine Protokolldatei gefunden" + +msgid "SeriesPlugin" +msgstr "SeriesPlugin" + +msgid "Show series info (SP)" +msgstr "Serien-Info anzeigen (SP)" + +msgid "Rename serie(s) (SP)" +msgstr "Serie(n) umbenennen (SP)" + +msgid "Check timer list for series (SP)" +msgstr "Prüfe Timerliste auf Serien Infos (SP)" + +msgid "" +" (C) 2012 by betonme @ IHAD \n" +"\n" +msgstr "" +" (C) 2012 von betonme @ IHAD \n" +"\n" + +msgid " Terms: " +msgstr " Bedingungen: " + +msgid " {lookups:d} successful lookups.\n" +msgstr " {lookups:d} Erfolgreiche Treffer.\n" + +msgid "" +" How much time have You saved?\n" +"\n" +msgstr "" +" Wieviel Zeit hast Du gespart?\n" +"\n" + +msgid " Support: " +msgstr " Unterstützung: " + +msgid " Feel free to donate. \n" +msgstr " Fühl dich frei zu Spenden. \n" + +msgid " PayPal: " +msgstr " PayPal: " + +msgid "SeriesPlugin test exception " +msgstr "SeriesPlugin Ausnahme Test" + +msgid "SeriesPlugin setup exception " +msgstr "SeriesPlugin Ausnahme Einstellungen" + +msgid "SeriesPlugin info exception " +msgstr "SeriesPlugin Ausnahme Information" + +msgid "SeriesPlugin extension exception " +msgstr "SeriesPlugin Ausnahme Erweiterung" + +msgid "SeriesPlugin renamer exception " +msgstr "SeriesPlugin Ausnahme Umbenennen" + +msgid "SeriesPlugin label exception " +msgstr "SeriesPlugin Ausnahme Beschriftung" + +msgid "Setup" +msgstr "Einstellungen" diff --git a/seriesplugin/src/Cacher.py b/seriesplugin/src/Cacher.py new file mode 100644 index 000000000..56bfb4389 --- /dev/null +++ b/seriesplugin/src/Cacher.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import sys +from time import time + +from Components.config import * + +from Logger import log + + +# Global cache +# Do we have to cleanup it +cache = {} + +def clearCache(): + global cache + cache = {} + + +class Cacher(object): + def __init__(self): + # This dict structure will be the following: + # { 'URL': (TIMESTAMP, value) } + #self.cache = {} + #global cache + #cache = {} + + # Max Age (in seconds) of each feed in the cache + self.expiration = config.plugins.seriesplugin.caching_expiration.value * 60 * 60 + + def getCached(self, url): + #pullCache + global cache + + if not config.plugins.seriesplugin.caching.value: + return + + # Try to get the tuple (TIMESTAMP, FEED_STRUCT) from the dict if it has + # already been downloaded. Otherwise assign None to already_got + already_got = cache.get(url, None) + + # Ok guys, we got it cached, let's see what we will do + if already_got: + # Well, it's cached, but will it be recent enough? + elapsed_time = time() - already_got[0] + + # Woooohooo it is, elapsed_time is less than INTER_QUERY_TIME so I + # can get the page from the memory, recent enough + if elapsed_time < self.expiration: + #log.debug("####SPCACHE GET ", already_got) + return already_got[1] + + else: + # Uhmmm... actually it's a bit old, I'm going to get it from the + # Net then, then I'll parse it and then I'll try to memoize it + # again + return None + + else: + # Well... We hadn't it cached in, so we need to get it from the Net + # now, It's useless to check if it's recent enough, it's not there. + return None + + def doCachePage(self, url, page): + global cache + + if not page: + log.debug("Cache: Got empty page") + return + + if not config.plugins.seriesplugin.caching.value: + return + + cache[url] = ( time(), page ) + + def doCacheList(self, url, list): + global cache + + if not list: + log.debug("Cache: Got empty list") + return + + if not config.plugins.seriesplugin.caching.value: + return + + cache[url] = ( time(), list ) diff --git a/seriesplugin/src/ChannelEditor.py b/seriesplugin/src/ChannelEditor.py new file mode 100644 index 000000000..4b0f667ac --- /dev/null +++ b/seriesplugin/src/ChannelEditor.py @@ -0,0 +1,437 @@ +# -*- coding: utf-8 -*- +from __init__ import _ + +import sys, os, base64, re, time, shutil, datetime, codecs, urllib2 + +from Components.ActionMap import ActionMap, HelpableActionMap +from Components.MenuList import MenuList +from Components.Button import Button +from Screens.Screen import Screen + +from Tools.BoundFunction import boundFunction +from Components.config import config + +from Screens.HelpMenu import HelpableScreen +from Screens.ChoiceBox import ChoiceBox +from Screens.MessageBox import MessageBox + +from enigma import eListboxPythonMultiContent, eListbox, gFont, RT_HALIGN_LEFT, RT_HALIGN_RIGHT, RT_HALIGN_CENTER, loadPNG, RT_WRAP, RT_VALIGN_CENTER, RT_VALIGN_TOP, RT_VALIGN_BOTTOM +from Tools.Directories import resolveFilename, SCOPE_PLUGINS, SCOPE_CURRENT_PLUGIN +from twisted.web import client, error as weberror +from twisted.internet import reactor, defer +from urllib import urlencode +from skin import parseColor, parseFont, parseSize + +try: + from skin import TemplatedListFonts +except: + TemplatedListFonts = None + +from difflib import SequenceMatcher + +#Internal +from Channels import ChannelsBase, buildSTBchannellist, unifyChannel, getTVBouquets, lookupChannelByReference +from Logger import log +from WebChannels import WebChannels + +# Constants +PIXMAP_PATH = resolveFilename(SCOPE_CURRENT_PLUGIN, "Extensions/SeriesPlugin/Images/" ) + +colorRed = 0xf23d21 +colorGreen = 0x389416 +colorBlue = 0x0064c7 +colorYellow = 0xbab329 +colorWhite = 0xffffff + + +class MatchList(MenuList): + """Defines a simple Component to show Timer name""" + + def __init__(self): + MenuList.__init__(self, [], enableWrapAround=True, content=eListboxPythonMultiContent) + + self.listFont = None + self.itemHeight = 30 + self.iconPosX = 8 + self.iconPosY = 8 + self.iconSize = 16 + self.colWidthStb = 300 + self.colWidthWeb = 250 + self.margin = 5 + + self.l.setBuildFunc(self.buildListboxEntry) + + global TemplatedListFonts + if TemplatedListFonts is not None: + tlf = TemplatedListFonts() + self.l.setFont(0, gFont(tlf.face(tlf.MEDIUM), tlf.size(tlf.MEDIUM))) + else: + self.l.setFont(0, gFont('Regular', 20 )) + + def applySkin(self, desktop, parent): + # This is a very bad way to get the skin attributes + # This function is called for every skin element, we should parse the attributes depending on the element name + attribs = [ ] + if self.skinAttributes is not None: + for (attrib, value) in self.skinAttributes: + if attrib == "font": + self.listFont = parseFont(value, ((1,1),(1,1))) + self.l.setFont(0, self.listFont) + elif attrib == "itemHeight": + self.itemHeight = int(value) + self.l.setItemHeight(self.itemHeight) + elif attrib == "iconPosX": + self.iconPosX = int(value) + elif attrib == "iconPosY": + self.iconPosY = int(value) + elif attrib == "iconSize": + self.iconSize = int(value) + elif attrib == "colWidthStb": + self.colWidthStb = int(value) + elif attrib == "colWidthWeb": + self.colWidthWeb = int(value) + elif attrib == "margin": + self.margin = int(value) + else: + attribs.append((attrib, value)) + self.skinAttributes = attribs + return MenuList.applySkin(self, desktop, parent) + + def buildListboxEntry(self, stbSender, webSender, serviceref, status): + + size = self.l.getItemSize() + + if int(status) == 0: + imageStatus = path = os.path.join(PIXMAP_PATH, "minus.png") + else: + imageStatus = path = os.path.join(PIXMAP_PATH, "plus.png") + + l = [(stbSender, webSender, serviceref, status),] + + pos = self.margin + self.iconPosX + l.append( (eListboxPythonMultiContent.TYPE_PIXMAP_ALPHATEST, pos, self.iconPosY, self.iconSize, self.iconSize, loadPNG(imageStatus)) ) + + pos += self.iconSize + self.margin + l.append( (eListboxPythonMultiContent.TYPE_TEXT, pos, 0, self.colWidthStb, self.itemHeight, 0, RT_HALIGN_LEFT | RT_VALIGN_CENTER, stbSender) ) + + pos += self.colWidthStb + self.margin + l.append( (eListboxPythonMultiContent.TYPE_TEXT, pos, 0, self.colWidthWeb, self.itemHeight, 0, RT_HALIGN_LEFT | RT_VALIGN_CENTER, webSender) ) + + pos += self.colWidthWeb + self.margin + l.append( (eListboxPythonMultiContent.TYPE_TEXT, pos, 0, size.width()-pos, self.itemHeight, 0, RT_HALIGN_LEFT | RT_VALIGN_CENTER, "", colorYellow) ) + + return l + + +class ChannelEditor(Screen, HelpableScreen, ChannelsBase, WebChannels): + + skinfile = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/Skins/ChannelEditor.xml" ) + skin = open(skinfile).read() + + def __init__(self, session): + Screen.__init__(self, session) + HelpableScreen.__init__(self) + ChannelsBase.__init__(self) + WebChannels.__init__(self) + + self.session = session + + self.skinName = [ "SeriesPluginChannelEditor" ] + + log.debug("ChannelEditor") + + from plugin import NAME, VERSION + self.setup_title = NAME + " " + _("Channel Editor") + " " + VERSION + + # Buttons + self["key_red"] = Button(_("Cancel")) + self["key_green"] = Button(_("OK")) + self["key_blue"] = Button(_("Remove")) + self["key_yellow"] = Button(_("Auto match")) + + # Define Actions + self["actions_1"] = HelpableActionMap(self, "SetupActions", { + "ok" : (self.keyAdd, _("Show popup to add Stb Channel")), + "cancel" : (self.keyCancel, _("Cancel and close")), + "deleteForward" : (self.keyResetChannelMapping, _("Reset channels")), + }, -1) + self["actions_2"] = HelpableActionMap(self, "DirectionActions", { + "left" : (self.keyLeft, _("Previeous page")), + "right" : (self.keyRight, _("Next page")), + "up" : (self.keyUp, _("One row up")), + "down" : (self.keyDown, _("One row down")), + }, -1) + self["actions_3"] = HelpableActionMap(self, "ChannelSelectBaseActions", { + "nextBouquet": (self.nextBouquet, _("Next bouquet")), + "prevBouquet": (self.prevBouquet, _("Previous bouquet")), + }, -1) + self["actions_4"] = HelpableActionMap(self, "ColorActions", { + "red" : (self.keyCancel, _("Cancel and close")), + "green" : (self.keySave, _("Save and close")), + "blue" : (self.keyRemove, _("Remove channel")), + "yellow" : (self.tryToMatchChannels, _("Auto match")), + }, -2) # higher priority + + self.helpList[0][2].sort() + + self["helpActions"] = ActionMap(["HelpActions",], { + "displayHelp" : self.showHelp + }, 0) + + self['list'] = MatchList() + self['list'].show() + + self.stbChlist = [] + self.webChlist = [] + self.stbToWebChlist = [] + + self.bouquet = None + + self.onLayoutFinish.append(self.readChannels) + self.onShown.append(self.showMessage) + + def showMessage(self): + if self.showMessage in self.onShown: + self.onShown.remove(self.showMessage) + self.session.open( MessageBox, _("If You are using SD and HD channels in parallel, You have to match both channels separately!"), MessageBox.TYPE_INFO ) + + def readChannels(self, bouquet=None): + self.stbToWebChlist = [] + + if bouquet is None: + self.bouquet = config.plugins.seriesplugin.bouquet_main.value + self.stbChlist = [] + elif bouquet != self.bouquet: + self.bouquet = bouquet + self.stbChlist = [] + + if not self.stbChlist: + self.loadStbChannels() + + if not self.webChlist: + self.loadWebChannels() + + self.showChannels() + + def loadStbChannels(self): + self.setTitle(_("Load STB channels for bouquet") + " " + self.bouquet) + log.debug("Load STB") + self.stbChlist = buildSTBchannellist(self.bouquet) + + def loadWebChannels(self): + self.setTitle(_("Load Web channels for bouquet") + " " + self.bouquet) + log.debug("Load Web channels") + data = self.getWebChannels() + if data: + temp = [ (x,unifyChannel(x)) for x in data] + else: + self.setTitle(_("Problem during loading Webchannels")) + temp = [] + self.webChlist = sorted(temp, key=lambda tup: tup[0]) + + def getChannelByRef(ref): + if self.stbChlist: + for servicename,serviceref,uservicename in self.stbChlist: + if serviceref == ref: + return servicename + return "" + + def showChannels(self): + self.setTitle(_("STB- / Web-Channel for bouquet:") + " " + self.bouquet ) + if len(self.stbChlist) != 0: + for servicename,serviceref,uservicename in self.stbChlist: + #log.debug("servicename", servicename, uservicename) + + webSender = lookupChannelByReference(serviceref) + if webSender is not False: + self.stbToWebChlist.append((servicename, ' / '.join(webSender), serviceref, "1")) + + else: + self.stbToWebChlist.append((servicename, "", serviceref, "0")) + + if len(self.stbToWebChlist) != 0: + self['list'].setList( self.stbToWebChlist ) + else: + log.debug("Error creating webChlist..") + self.setTitle(_("Error check log file")) + + def tryToMatchChannels(self): + self.setTitle(_("Channel matching...")) + self.stbToWebChlist = [] + sequenceMatcher = SequenceMatcher(" ".__eq__, "", "") + + if len(self.stbChlist) != 0: + for servicename,serviceref,uservicename in self.stbChlist: + #log.debug("servicename", servicename, uservicename) + + webSender = lookupChannelByReference(serviceref) + if webSender is not False: + self.stbToWebChlist.append((servicename, ' / '.join(webSender), serviceref, "1")) + + else: + if len(self.webChlist) != 0: + match = "" + ratio = 0 + for webSender, uwebSender in self.webChlist: + #log.debug("webSender", webSender, uwebSender) + if uwebSender in uservicename or uservicename in uwebSender: + + sequenceMatcher.set_seqs(uservicename, uwebSender) + newratio = sequenceMatcher.ratio() + if newratio > ratio: + log.debug("possible match", servicename, uservicename, webSender, uwebSender, ratio) + ratio = newratio + match = webSender + + if ratio > 0: + log.debug("match", servicename, uservicename, match, ratio) + self.stbToWebChlist.append((servicename, match, serviceref, "1")) + self.addChannel(serviceref, servicename, match) + + else: + self.stbToWebChlist.append((servicename, "", serviceref, "0")) + + else: + self.stbToWebChlist.append((servicename, "", serviceref, "0")) + + if len(self.stbToWebChlist) != 0: + self['list'].setList( self.stbToWebChlist ) + else: + log.debug("Error creating webChlist..") + self.setTitle(_("Error check log file")) + + def getIndexOfWebSender(self, webSender): + for pos,webCh in enumerate(self.webChlist): + if(webCh[0] == webSender): + return pos + return 0 + + def keyAdd(self): + check = self['list'].getCurrent() + if check == None: + log.debug("list empty") + return + else: + idx = 0 + servicename, webSender, serviceref, state = check + idx = 0 + if webSender: + idx = self.getIndexOfWebSender(self.webChlist) + log.debug("keyAdd webSender", webSender, idx) + self.session.openWithCallback( boundFunction(self.addConfirm, servicename, serviceref, webSender), ChoiceBox,_("Add Web Channel"), self.webChlist, None, idx) + + def getIndexOfServiceref(self, serviceref): + for pos,stbWebChl in enumerate(self.stbToWebChlist): + if(stbWebChl[2] == serviceref): + return pos + return False + + def addConfirm(self, servicename, serviceref, webSender, result): + if not result: + return + remote = result[0] + if webSender and remote == webSender: + log.debug("addConfirm skip already set", servicename, serviceref, remote, webSender) + elif servicename and serviceref and remote and not webSender: + idx = self.getIndexOfServiceref(serviceref) + log.debug("addConfirm", servicename, serviceref, remote, idx) + if idx is not False: + self.setTitle(_("Channel '- %(servicename)s - %(remote)s -' added.") % {'servicename': servicename, 'remote':remote } ) + self.addChannel(serviceref, servicename, remote) + self.stbToWebChlist[idx] = (servicename, remote, serviceref, "1") + self['list'].setList( self.stbToWebChlist ) + elif servicename and serviceref and remote and webSender: + log.debug("add or replace", servicename, serviceref, remote, webSender) + self.session.openWithCallback( boundFunction(self.addOrReplace, servicename, serviceref, webSender, remote), MessageBox,_("Add channel (Yes) or replace it (No)"), MessageBox.TYPE_YESNO, default = False) + + def addOrReplace(self, servicename, serviceref, webSender, remote, result): + idx = self.getIndexOfServiceref(serviceref) + log.debug("addOrReplace", servicename, serviceref, remote, webSender, idx) + if idx is False: + return + + if result: + log.debug("add", servicename, serviceref, remote, webSender) + self.setTitle(_("Channel '- %(servicename)s - %(remote)s -' added.") % {'servicename': servicename, 'remote':remote } ) + self.addChannel(serviceref, servicename, remote) + self.stbToWebChlist[idx] = (servicename, webSender+" / "+remote, serviceref, "1") + + else: + log.debug("replace", servicename, serviceref, remote, webSender) + self.setTitle(_("Channel '- %(servicename)s - %(remote)s -' replaced.") % {'servicename': servicename, 'remote':remote } ) + self.replaceChannel(serviceref, servicename, remote) + self.stbToWebChlist[idx] = (servicename, remote, serviceref, "1") + + self['list'].setList( self.stbToWebChlist ) + + def keyRemove(self): + check = self['list'].getCurrent() + if check == None: + log.debug("keyRemove list empty") + return + else: + servicename, webSender, serviceref, state = check + log.debug("keyRemove", servicename, webSender, serviceref, state) + if serviceref: + #TODO handle multiple links/alternatives - show a choicebox + self.session.openWithCallback( boundFunction(self.removeConfirm, servicename, serviceref), MessageBox, _("Remove '%s'?") % servicename, MessageBox.TYPE_YESNO, default = False) + + def removeConfirm(self, servicename, serviceref, answer): + if not answer: + return + if serviceref: + idx = self.getIndexOfServiceref(serviceref) + if idx is not False: + log.debug("removeConfirm", servicename, serviceref, idx) + self.setTitle(_("Channel '- %s -' removed.") % servicename) + self.removeChannel(serviceref) + self.stbToWebChlist[idx] = (servicename, "", serviceref, "0") + self['list'].setList( self.stbToWebChlist ) + + def keyResetChannelMapping(self): + self.session.openWithCallback(self.channelReset, MessageBox, _("Reset channel list?"), MessageBox.TYPE_YESNO) + + def channelReset(self, answer): + if answer: + log.debug("channel-list reset...") + self.resetChannels() + self.stbChlist = [] + self.webChlist = [] + self.stbToWebChlist = [] + self.readChannels() + + def keyLeft(self): + self['list'].pageUp() + + def keyRight(self): + self['list'].pageDown() + + def keyDown(self): + self['list'].down() + + def keyUp(self): + self['list'].up() + + def nextBouquet(self): + tvbouquets = getTVBouquets() + next = tvbouquets[0][1] + for tvbouquet in reversed(tvbouquets): + if tvbouquet[1] == self.bouquet: + break + next = tvbouquet[1] + self.readChannels(next) + + def prevBouquet(self): + tvbouquets = getTVBouquets() + prev = tvbouquets[-1][1] + for tvbouquet in tvbouquets: + if tvbouquet[1] == self.bouquet: + break + prev = tvbouquet[1] + self.readChannels(prev) + + def keySave(self): + self.close(ChannelsBase.channels_changed) + + def keyCancel(self): + self.close(False) diff --git a/seriesplugin/src/Channels.py b/seriesplugin/src/Channels.py new file mode 100644 index 000000000..7ae870a3f --- /dev/null +++ b/seriesplugin/src/Channels.py @@ -0,0 +1,342 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import os +import re + +# Config +from Components.config import config + +from enigma import eServiceReference, eServiceCenter +from ServiceReference import ServiceReference + +from Tools.BoundFunction import boundFunction + +# XML +from xml.etree.cElementTree import ElementTree, parse, Element, SubElement, Comment +from Tools.XMLTools import stringToXML + +# Plugin internal +from . import _ +from XMLFile import XMLFile, indent +from Logger import log + +try: + #Python >= 2.7 + from collections import OrderedDict +except: + from OrderedDict import OrderedDict + + +ChannelReplaceDict = OrderedDict([ + ('\(S\)', ''), + (' HD', ''), + (' TV', ''), + (' Television', ''), + (' Channel', ''), + ('III', 'drei'), + ('II', 'zwei'), + #('I', 'eins'), + ('ARD', 'daserste'), + ('\+', 'plus'), + ('0', 'null'), + ('1', 'eins'), + ('2', 'zwei'), + ('3', 'drei'), + ('4', 'vier'), + ('5', 'fuenf'), + ('6', 'sechs'), + ('7', 'sieben'), + ('8', 'acht'), + ('9', 'neun'), + ('\xc3\xa4', 'ae'), + ('\xc3\xb6', 'oe'), + ('\xc3\xbc', 'ue'), + ('\xc3\x84', 'ae'), + ('\xc3\x96', 'oe'), + ('\xc3\x9c', 'ue'), + ('\xc3\x9f', 'ss'), +]) +CompiledRegexpChannelUnify = re.compile('|'.join(ChannelReplaceDict)) +CompiledRegexpChannelRemoveSpecialChars = re.compile('[^a-zA-Z0-9]') +def unifyChannel(text): + def translate(match): + m = match.group(0) + return ChannelReplaceDict.get(m, m) + + text = CompiledRegexpChannelUnify.sub(translate, text) + try: + text = text.decode("utf-8").encode("latin1") + except: + pass + text = CompiledRegexpChannelRemoveSpecialChars.sub('', text) + return text.strip().lower() + + +def getServiceList(ref): + root = eServiceReference(str(ref)) + serviceHandler = eServiceCenter.getInstance() + return serviceHandler.list(root).getContent("SN", True) + +def getTVBouquets(): + from Screens.ChannelSelection import service_types_tv + return getServiceList(service_types_tv + ' FROM BOUQUET "bouquets.tv" ORDER BY bouquet') + +def getServicesOfBouquet(bouquet): + bouquetlist = getServiceList(bouquet) + chlist = [] + for (serviceref, servicename) in bouquetlist: + + if (eServiceReference(serviceref).flags & eServiceReference.isDirectory): + # handle directory services + log.debug("SPC: found directory %s" % (serviceref) ) + chlist.extend( getServicesOfBouquet(serviceref) ) + + elif (eServiceReference(serviceref).flags & eServiceReference.isGroup): + # handle group services + log.debug("SPC: found group %s" % (serviceref) ) + chlist.append((servicename, re.sub('::.*', ':', serviceref), unifyChannel(servicename))) + chlist.extend( getServicesOfBouquet(serviceref) ) + + elif not (eServiceReference(serviceref).flags & eServiceReference.isMarker): + # playable + log.debug("SPC: found playable service %s" % (serviceref) ) + chlist.append((servicename, re.sub('::.*', ':', serviceref), unifyChannel(servicename))) + + return chlist + +def buildSTBchannellist(BouquetName = None): + chlist = [] + tvbouquets = getTVBouquets() + log.debug("SPC: found %s bouquet: %s" % (len(tvbouquets), tvbouquets) ) + + if not BouquetName: + for bouquet in tvbouquets: + chlist.extend( getServicesOfBouquet(bouquet[0]) ) + else: + for bouquet in tvbouquets: + if bouquet[1] == BouquetName: + chlist.extend( getServicesOfBouquet(bouquet[0]) ) + + return chlist + +def getChannel(ref): + if isinstance(ref, eServiceReference): + servicereference = ServiceReference(ref) + elif isinstance(ref, ServiceReference): + servicereference = ref + else: + servicereference = ServiceReference(str(ref)) + if servicereference: + return servicereference.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '') + return "" + +def compareChannels(ref, remote): + log.debug("compareChannels", ref, remote) + remote = remote.lower() + if ref in ChannelsBase.channels: + ( name, alternatives ) = ChannelsBase.channels[ref] + for altname in alternatives: + if altname.lower() in remote or remote in altname.lower(): + return True + + return False + +def lookupChannelByReference(ref): + if ref in ChannelsBase.channels: + ( name, alternatives ) = ChannelsBase.channels[ref] + altnames = [] + for altname in alternatives: + if altname: + log.debug("lookupChannelByReference", ref, altname) + altnames.append(altname) + return altnames + log.debug("lookupChannelByReference: Failed for", ref) + return False + + +class ChannelsBase(XMLFile): + + channels = {} # channels[reference] = ( name, [ name1, name2, ... ] ) + channels_changed = False + + def __init__(self): + + path = config.plugins.seriesplugin.channel_file.value + XMLFile.__init__(self, path) + + self.resetChannels() + + def channelsEmpty(self): + return not ChannelsBase.channels + + def resetChannels(self): + ChannelsBase.channels = {} + ChannelsBase.channels_changed = False + + self.loadXML() + + def addChannel(self, ref, name, remote): + log.debug("SP addChannel name remote", name, remote) + + if ref in ChannelsBase.channels: + ( name, alternatives ) = ChannelsBase.channels[ref] + if remote not in alternatives: + alternatives.append(remote) + ChannelsBase.channels[ref] = ( name, alternatives ) + else: + ChannelsBase.channels[ref] = ( name, [remote] ) + ChannelsBase.channels_changed = True + + def replaceChannel(self, ref, name, remote): + log.debug("SP addChannel name remote", name, remote) + + ChannelsBase.channels[ref] = ( name, [remote] ) + ChannelsBase.channels_changed = True + + def removeChannel(self, ref): + if ref in ChannelsBase.channels: + del ChannelsBase.channels[ref] + ChannelsBase.channels_changed = True + + # + # I/O Functions + # + def loadXML(self): + try: + # Read xml config file + etree = self.readXML() + if etree: + channels = {} + + # Parse Config + def parse(root): + channels = {} + version = root.get("version", "1") + if version.startswith("1"): + log.warning( _("Skipping old channels file") ) + elif version.startswith("2") or version.startswith("3") or version.startswith("4"): + log.debug("Channel XML Version 4") + ChannelsBase.channels_changed = True + if root: + for element in root.findall("Channel"): + name = element.get("name", "") + reference = element.get("reference", "") + if name and reference: + alternatives = [] + for alternative in element.findall("Alternative"): + alternatives.append( alternative.text ) + channels[reference] = (name, list(set(alternatives))) + log.debug("Channel", reference, channels[reference] ) + else: + # XMLTV compatible channels file + log.debug("Channel XML Version 5") + if root: + for element in root.findall("channel"): + alternatives = [] + id = element.get("id", "") + alternatives.append( id ) + name = element.get("name", "") + reference = element.text + #Test customization but XML conform + for web in element.findall("web"): + alternatives.append( web.text ) + channels[reference] = (name, list(set(alternatives))) + log.debug("Channel", reference, channels[reference] ) + return channels + + channels = parse( etree.getroot() ) + log.debug("Channel XML load", len(channels)) + else: + channels = {} + ChannelsBase.channels = channels + except Exception as e: + log.exception("Exception in loadXML: " + str(e)) + + def saveXML(self): + try: + if ChannelsBase.channels_changed: + + ChannelsBase.channels_changed = False + + channels = ChannelsBase.channels + + # Generate List in RAM + etree = None + #log.debug("saveXML channels", channels) + log.debug("SP saveXML channels", len(channels)) + + # XMLTV compatible channels file + #TEST Do we need to write the xml header node + + # Build Header + from plugin import NAME, VERSION + root = Element("channels") + root.set('version', VERSION) + root.set('created_by', NAME) + root.append(Comment(_("Don't edit this manually unless you really know what you are doing"))) + + # Build Body + def build(root, channels): + if channels: + for reference, namealternatives in channels.iteritems(): + name, alternatives = namealternatives[:] + if alternatives: + # Add channel + web = alternatives[0] + element = SubElement( root, "channel", name = stringToXML(name), id = stringToXML(web) ) + element.text = stringToXML(reference) + del alternatives[0] + if alternatives: + for web in alternatives: + SubElement( element, "web" ).text = stringToXML(web) + return root + + etree = ElementTree( build( root, channels ) ) + + indent(etree.getroot()) + + self.writeXML( etree ) + + if config.plugins.seriesplugin.epgimport.value: + log.debug("Write: xml channels for epgimport") + try: + path = "/etc/epgimport/wunschliste.channels.xml" + etree.write(path, encoding='utf-8', xml_declaration=True) + except Exception as e: + log.exception("Exception in write XML: " + str(e)) + + if config.plugins.seriesplugin.xmltvimport.value: + log.debug("Write: xml channels for xmltvimport") + try: + path = "/etc/xmltvimport/wunschliste.channels.xml" + etree.write(path, encoding='utf-8', xml_declaration=True) + except Exception as e: + log.exception("Exception in write XML: " + str(e)) + + if config.plugins.seriesplugin.crossepg.value: + log.debug("Write: xml channels for crossepg") + try: + path = "/etc/crossepg/wunschliste.channels.xml" + etree.write(path, encoding='utf-8', xml_declaration=True) + except Exception as e: + log.exception("Exception in write XML: " + str(e)) + + except Exception as e: + log.exception("Exception in writeXML: " + str(e)) diff --git a/seriesplugin/src/DirectoryPatterns.py b/seriesplugin/src/DirectoryPatterns.py new file mode 100644 index 000000000..249d080f7 --- /dev/null +++ b/seriesplugin/src/DirectoryPatterns.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import os +import json + +# for localized messages +from . import _ + +# Config +from Components.config import * + +# Plugin internal +from Logger import log + + +scheme_fallback = [ + ("Off", "Disabled"), + + ("{org:s}/{series:s}/" , "Original/Series/"), + + ("{org:s}/{series:s}/{season:02d}/" , "Original/Series/01/"), + ("{org:s}/{series:s}/S{season:02d}/" , "Original/Series/S01/"), + ("{org:s}/{series:s}/{rawseason:s}/" , "Original/Series/Raw/"), + + ("{org:s}/{series:s}/Season {season:02d}/" , "Original/Series/Season 01/"), + ("{org:s}/{series:s}/Season {rawseason:s}/" , "Original/Series/Season Raw/"), + + ("{org:s}/{series:s} {season:02d}/" , "Original/Series 01/"), + ("{org:s}/{series:s} S{season:02d}/" , "Original/Series S01/"), + + ("{org:s}/{series:s} Season {season:02d}/" , "Original/Series Season 01/"), + ("{org:s}/{series:s} Season {rawseason:s}/" , "Original/Series Season Raw/"), + + ("{org:s}/{service:s}/{series:s}/Season {rawseason:s}/" , "Original/Service/Series/Season Raw/"), + ("{org:s}/{channel:s}/{series:s}/Season {rawseason:s}/" , "Original/Channel/Series/Season Raw/"), + + ("{org:s}/{date:s}/{series:s}/" , "Date/Series/"), + ("{org:s}/{time:s}/{series:s}/" , "Time/Series/") + ] + +def readDirectoryPatterns(): + path = config.plugins.seriesplugin.pattern_file_directories.value + obj = None + patterns = None + + if os.path.exists(path): + log.debug("Found directory pattern file") + f = None + try: + f = open(path, 'rb') + header, patterns = json.load(f) + patterns = [tuple(p) for p in patterns] + except Exception as e: + log.exception(_("Your pattern file is corrupt") + "\n" + path + "\n\n" + str(e)) + finally: + if f is not None: + f.close() + return patterns or scheme_fallback diff --git a/seriesplugin/src/FilePatterns.py b/seriesplugin/src/FilePatterns.py new file mode 100644 index 000000000..6b1a30ce9 --- /dev/null +++ b/seriesplugin/src/FilePatterns.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import os +import json + +# for localized messages +from . import _ + +# Config +from Components.config import * + +# Plugin internal +from Logger import log + + +scheme_fallback = [ + ("Off", "Disabled"), + + ("{org:s} S{season:02d}E{episode:02d}" , "Org S01E01"), + ("{org:s} S{season:d}E{episode:d}" , "Org S1E1"), + + ("{org:s} S{season:02d}E{episode:02d} {title:s}" , "Org S01E01 Title"), + ("{org:s} S{season:d}E{episode:d} {title:s}" , "Org S1E1 Title"), + + ("{org:s} {title:s} S{season:02d}E{episode:02d}" , "Org Title S01E01"), + ("{org:s} {title:s} S{season:d}E{episode:d}" , "Org Title S1E1"), + + ("{org:s} - S{season:02d}E{episode:02d} - {title:s}" , "Org - S01E01 - Title"), + ("{org:s} - S{season:2d}E{episode:2d} - {title:s}" , "Org - S1E1 - Title"), + + ("S{season:02d}E{episode:02d}" , "S01E01"), + ("S{season:02d}E{episode:02d} {org:s}" , "S01E01 Org"), + ("S{season:d}E{episode:d} {org:s}" , "S1E1 Org"), + + ("S{season:02d}E{episode:02d} {title:s} {org:s}" , "S01E01 Title Org"), + ("S{season:d}E{episode:d} {title:s} {org:s}" , "S1E1 Title Org"), + + ("{title:s} S{season:02d}E{episode:02d} {org:s}" , "Title S01E01 Org"), + ("{title:s} S{season:d}E{episode:d} {org:s}" , "Title S1E1 Org"), + + ("{title:s}" , "Title"), + ("{title:s} {org:s}" , "Title Org"), + ("{title:s} {series:s}" , "Title Series"), + + ("{org:s} {title:s}" , "Org Title"), + ("{series:s} {title:s}" , "Series Title"), + + ("{series:s} S{season:02d}E{episode:02d}" , "Series S01E01"), + ("{series:s} S{season:d}E{episode:d}" , "Series S1E1"), + + ("{series:s} S{season:02d}E{episode:02d} {title:s}" , "Series S01E01 Title"), + ("{series:s} S{season:d}E{episode:d} {title:s}" , "Series S1E1 Title"), + + ("{series:s} {title:s} S{season:02d}E{episode:02d}" , "Series Title S01E01"), + ("{series:s} {title:s} S{season:d}E{episode:d}" , "Series Title S1E1"), + + ("S{season:02d}E{episode:02d} {series:s}" , "S01E01 Series"), + ("S{season:d}E{episode:d} {series:s}" , "S1E1 Series"), + + ("S{season:02d}E{episode:02d} {title:s} {series:s}" , "S01E01 Title Series"), + ("S{season:d}E{episode:d} {title:s} {series:s}" , "S1E1 Title Series"), + + ("{title:s} S{season:02d}E{episode:02d} {series:s}" , "Title S01E01 Series"), + ("{title:s} S{season:d}E{episode:d} {series:s}" , "Title S1E1 Series"), + + ("{series:s} - s{season:02d}e{episode:02d} - {title:s}" , "Series - s01e01 - Title"), + ("{series:s} - S{season:02d}E{episode:02d} - {title:s}" , "Series - S01E01 - Title"), + + ("{org:s}_S{season:02d}EP{episode:02d}" , "Org_S01EP01"), + ("{org:s}_S{season:02d}EP{episode:02d_}" , "Org_S01EP01_"), + + + ("{org:s} S{season:02d} E{rawepisode:s} {title:s}" , "Org S01 ERaw Title"), + ("{org:s} S{season:02d}E{rawepisode:s} {title:s}" , "Org S01ERaw Title"), + ("{org:s} {season:02d} {rawepisode:s} {title:s}" , "Org 01 Raw Title"), + ("{org:s} {season:02d}{rawepisode:s} {title:s}" , "Org 01Raw Title"), + + ("{org:s} - S{season:02d} E{rawepisode:s} - {title:s}" , "Org - S01 ERaw - Title"), + ("{org:s} - S{season:02d}E{rawepisode:s} - {title:s}" , "Org - S01ERaw - Title"), + ("{org:s} - {season:02d} {rawepisode:s} - {title:s}" , "Org - 01 Raw - Title"), + ("{org:s} - {season:02d}{rawepisode:s} - {title:s}" , "Org - 01Raw - Title"), + + ("{series:s} S{season:02d} E{rawepisode:s} {title:s}" , "Series S01 ERaw Title"), + ("{series:s} S{season:02d}E{rawepisode:s} {title:s}" , "Series S01ERaw Title"), + ("{series:s} {season:02d} {rawepisode:s} {title:s}" , "Series 01 Raw Title"), + ("{series:s} {season:02d}{rawepisode:s} {title:s}" , "Series 01Raw Title"), + + ("{series:s} - S{season:02d} E{rawepisode:s} - {title:s}" , "Series - S01 ERaw - Title"), + ("{series:s} - S{season:02d}E{rawepisode:s} - {title:s}" , "Series - S01ERaw - Title"), + ("{series:s} - {season:02d} {rawepisode:s} - {title:s}" , "Series - 01 Raw - Title"), + ("{series:s} - {season:02d}{rawepisode:s} - {title:s}" , "Series - 01Raw - Title"), + + + ("{org:s} S{rawseason:s} E{rawepisode:s} {title:s}" , "Org SRaw ERaw Title"), + ("{org:s} S{rawseason:s}E{rawepisode:s} {title:s}" , "Org SRawERaw Title"), + ("{org:s} {rawseason:s} {rawepisode:s} {title:s}" , "Org Raw Raw Title"), + ("{org:s} {rawseason:s}{rawepisode:s} {title:s}" , "Org RawRaw Title"), + + ("{org:s} - S{rawseason:s} E{rawepisode:s} - {title:s}" , "Org - SRaw ERaw - Title"), + ("{org:s} - S{rawseason:s}E{rawepisode:s} - {title:s}" , "Org - SRawERaw - Title"), + ("{org:s} - {rawseason:s} {rawepisode:s} - {title:s}" , "Org - Raw Raw - Title"), + ("{org:s} - {rawseason:s}{rawepisode:s} - {title:s}" , "Org - RawRaw - Title"), + + ("{series:s} S{rawseason:s} E{rawepisode:s} {title:s}" , "Series SRaw ERaw Title"), + ("{series:s} S{rawseason:s}E{rawepisode:s} {title:s}" , "Series SRawERaw Title"), + ("{series:s} {rawseason:s} {rawepisode:s} {title:s}" , "Series Raw Raw Title"), + ("{series:s} {rawseason:s}{rawepisode:s} {title:s}" , "Series RawRaw Title"), + + ("{series:s} - S{rawseason:s} E{rawepisode:s} - {title:s}" , "Series - SRaw ERaw - Title"), + ("{series:s} - S{rawseason:s}E{rawepisode:s} - {title:s}" , "Series - SRawERaw - Title"), + ("{series:s} - {rawseason:s} {rawepisode:s} - {title:s}" , "Series - Raw Raw - Title"), + ("{series:s} - {rawseason:s}{rawepisode:s} - {title:s}" , "Series - RawRaw - Title"), + + + ("{org:s} S{season:02d} E{rawepisode:s}" , "Org S01 ERaw"), + ("{org:s} S{season:02d}E{rawepisode:s}" , "Org S01ERaw"), + ("{org:s} {season:02d} {rawepisode:s}" , "Org 01 Raw"), + ("{org:s} {season:02d}{rawepisode:s}" , "Org 01Raw"), + + ("{org:s} - S{season:02d} E{rawepisode:s}" , "Org - S01 ERaw"), + ("{org:s} - S{season:02d}E{rawepisode:s}" , "Org - S01ERaw"), + ("{org:s} - {season:02d} {rawepisode:s}" , "Org - 01 Raw"), + ("{org:s} - {season:02d}{rawepisode:s}" , "Org - 01Raw"), + + ("{series:s} S{season:02d} E{rawepisode:s}" , "Series S01 ERaw"), + ("{series:s} S{season:02d}E{rawepisode:s}" , "Series S01ERaw"), + ("{series:s} {season:02d} {rawepisode:s}" , "Series 01 Raw"), + ("{series:s} {season:02d}{rawepisode:s}" , "Series 01Raw"), + + ("{series:s} - S{season:02d} E{rawepisode:s}" , "Series - S01 ERaw"), + ("{series:s} - S{season:02d}E{rawepisode:s}" , "Series - S01ERaw"), + ("{series:s} - {season:02d} {rawepisode:s}" , "Series - 01 Raw"), + ("{series:s} - {season:02d}{rawepisode:s}" , "Series - 01Raw"), + + + ("{channel:s} {series:s} S{season:02d} E{rawepisode:s} {title:s}" , "Channel Series S01 ERaw Title"), + ("{service:s} {series:s} S{season:02d}E{rawepisode:s} {title:s}" , "Service Series S01ERaw Title"), + + ("{date:s} {channel:s} {series:s} S{season:02d} E{rawepisode:s} {title:s}" , "Date Channel Series S01 ERaw Title"), + ("{date:s} {time:s} {channel:s} {series:s} S{season:02d} E{rawepisode:s} {title:s}" , "Date Time Channel Series S01 ERaw Title") + ] + +def readFilePatterns(): + path = config.plugins.seriesplugin.pattern_file.value + obj = None + patterns = None + + if os.path.exists(path): + log.debug("Found title pattern file") + f = None + try: + f = open(path, 'rb') + header, patterns = json.load(f) + patterns = [tuple(p) for p in patterns] + except Exception as e: + log.exception(_("Your pattern file is corrupt") + "\n" + path + "\n\n" + str(e)) + finally: + if f is not None: + f.close() + return patterns or scheme_fallback diff --git a/seriesplugin/src/IdentifierBase.py b/seriesplugin/src/IdentifierBase.py new file mode 100644 index 000000000..099e6081f --- /dev/null +++ b/seriesplugin/src/IdentifierBase.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# by betonme @2012 + +from collections import defaultdict + +from thread import start_new_thread + +#TODO Implement Twisted handler +#Twisted 12.x +#from twisted.web.client import getPage as twGetPage +#Twisted 8.x +#from twisted.web.client import _parse, HTTPClientFactory +#from twisted.internet import reactor +#Twisted All +#from twisted.python.failure import Failure + +from time import sleep + +from time import time +from datetime import datetime, timedelta + +from Components.config import config +from Tools.BoundFunction import boundFunction + +# Internal +from ModuleBase import ModuleBase +from Cacher import Cacher +from Logger import log + + +class MyException(Exception): + pass + +class IdentifierBase2(ModuleBase, Cacher): + def __init__(self): + ModuleBase.__init__(self) + Cacher.__init__(self) + + self.max_time_drift = int(config.plugins.seriesplugin.max_time_drift.value) * 60 + + self.name = "" + self.begin = None + self.end = None + self.channel = "" + self.ids = [] + + self.knownids = [] + + self.returnvalue = None + + self.search_depth = 0; + + self.now = time() + today = datetime.today() + self.actual_month = today.month + self.actual_year = today.year + + ################################################ + # Helper function + def getAlternativeSeries(self, name): + + self.search_depth += 1 + if( self.search_depth < config.plugins.seriesplugin.search_depths.value ): + + if self.search_depth == 1: + if name.find("-") != -1: + alt = " ".join(name.split("-")[:-1]).strip() + else: + alt = " ".join(name.split(" ")[:-1]) + else: + alt = " ".join(name.split(" ")[:-1]) + + # Avoid searchs with: The, Der, Die, Das... + if len(alt) > 3: + return alt + else: + return "" + else: + return "" + + def filterKnownIds(self, newids): + # Filter already checked series + filteredids = [elem for elem in newids if elem not in self.knownids] + + # Add new ids to knownid list + self.knownids.extend(filteredids) + + return filteredids + + ################################################ + # Service prototypes + @classmethod + def knowsElapsed(cls): + # True: Service knows elapsed air dates + # False: Service doesn't know elapsed air dates + return False + + @classmethod + def knowsToday(cls): + # True: Service knows today air dates + # False: Service doesn't know today air dates + return False + + @classmethod + def knowsFuture(cls): + # True: Service knows future air dates + # False: Service doesn't know future air dates + return False + + ################################################ + # To be implemented by subclass + def getLogo(self, future=True, today=False, elapsed=False): + # Return the name of the logo without extension .png + pass + + def getEpisode(self, name, begin, end, service): + # On Success: Return a single season, episode, title tuple + # On Failure: Return a empty list or String or None + return None diff --git a/seriesplugin/src/Identifiers/Makefile.am b/seriesplugin/src/Identifiers/Makefile.am new file mode 100644 index 000000000..1a74b7739 --- /dev/null +++ b/seriesplugin/src/Identifiers/Makefile.am @@ -0,0 +1,2 @@ +installdir = $(libdir)/enigma2/python/Plugins/Extensions/SeriesPlugin/Identifiers/ +install_PYTHON = *.py diff --git a/seriesplugin/src/Identifiers/SerienServer.py b/seriesplugin/src/Identifiers/SerienServer.py new file mode 100644 index 000000000..c49180e69 --- /dev/null +++ b/seriesplugin/src/Identifiers/SerienServer.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# by betonme @2012 + +# Imports +import re + +from Components.config import config + +from time import time, mktime +from datetime import datetime + +# Internal +from Plugins.Extensions.SeriesPlugin.__init__ import _ +from Plugins.Extensions.SeriesPlugin.IdentifierBase import IdentifierBase2 +from Plugins.Extensions.SeriesPlugin.Logger import log +from Plugins.Extensions.SeriesPlugin.Channels import lookupChannelByReference, getChannel +from Plugins.Extensions.SeriesPlugin.TimeoutServerProxy import TimeoutServerProxy + + +class SerienServer(IdentifierBase2): + def __init__(self): + IdentifierBase2.__init__(self) + + self.server = TimeoutServerProxy() + + @classmethod + def knowsElapsed(cls): + return True + + @classmethod + def knowsToday(cls): + return True + + @classmethod + def knowsFuture(cls): + return True + + def getLogo(self, future=True, today=False, elapsed=False): + if future: + return "Wunschliste" + elif today: + return "Wunschliste" + else: + return "Fernsehserien" + + def getEpisode(self, name, begin, end=None, service=None): + # On Success: Return a single season, episode, title tuple + # On Failure: Return a empty list or String or None + + + # Check preconditions + if not name: + msg =_("Skipping lookup because no show name is specified") + log.warning(msg) + return msg + if not begin: + msg = _("Skipping lookup because no begin timestamp is specified") + log.warning(msg) + return msg + if not service: + msg = _("Skipping lookup because no channel is specified") + log.warning(msg) + return msg + + + self.name = name + self.begin = begin + self.end = end + self.service = service + + log.info("SerienServer getEpisode, name, begin, end=None, service", name, begin, end, service) + + # Prepare parameters + webChannels = lookupChannelByReference(service) + if not webChannels: + msg = _("No matching channel found.") + "\n" + getChannel(service) + " (" + str(service) + ")\n\n" + _("Please open the Channel Editor and add the channel manually.") + log.warning(msg) + return msg + + unixtime = str(begin) + max_time_drift = self.max_time_drift + + # Lookup + for webChannel in webChannels: + log.debug("SerienServer getSeasonEpisode(): [\"%s\",\"%s\",\"%s\",%s]" % (name, webChannel, unixtime, max_time_drift)) + + result = self.server.getSeasonEpisode( name, webChannel, unixtime, self.max_time_drift ) + + if result and isinstance(result, dict): + result['service'] = service + result['channel'] = webChannel + result['begin'] = begin + + log.debug("SerienServer getSeasonEpisode result:", type(result), result) + + return result + + else: + return ( _("No match found") ) diff --git a/seriesplugin/src/Identifiers/__init__.py b/seriesplugin/src/Identifiers/__init__.py new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/seriesplugin/src/Identifiers/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/seriesplugin/src/Images/Makefile.am b/seriesplugin/src/Images/Makefile.am new file mode 100644 index 000000000..993e9237b --- /dev/null +++ b/seriesplugin/src/Images/Makefile.am @@ -0,0 +1,2 @@ +installdir = $(libdir)/enigma2/python/Plugins/Extensions/SeriesPlugin/Images +install_DATA = blue_round.png green_round.png minus.png plus.png red_round.png yellow_round.png diff --git a/seriesplugin/src/Images/blue_round.png b/seriesplugin/src/Images/blue_round.png new file mode 100644 index 000000000..51affae01 Binary files /dev/null and b/seriesplugin/src/Images/blue_round.png differ diff --git a/seriesplugin/src/Images/green_round.png b/seriesplugin/src/Images/green_round.png new file mode 100644 index 000000000..7b367f296 Binary files /dev/null and b/seriesplugin/src/Images/green_round.png differ diff --git a/seriesplugin/src/Images/minus.png b/seriesplugin/src/Images/minus.png new file mode 100644 index 000000000..39ab38d62 Binary files /dev/null and b/seriesplugin/src/Images/minus.png differ diff --git a/seriesplugin/src/Images/plus.png b/seriesplugin/src/Images/plus.png new file mode 100644 index 000000000..46bb29851 Binary files /dev/null and b/seriesplugin/src/Images/plus.png differ diff --git a/seriesplugin/src/Images/red_round.png b/seriesplugin/src/Images/red_round.png new file mode 100644 index 000000000..cbb2ba7c4 Binary files /dev/null and b/seriesplugin/src/Images/red_round.png differ diff --git a/seriesplugin/src/Images/yellow_round.png b/seriesplugin/src/Images/yellow_round.png new file mode 100644 index 000000000..dfcc9c5db Binary files /dev/null and b/seriesplugin/src/Images/yellow_round.png differ diff --git a/seriesplugin/src/LICENSE b/seriesplugin/src/LICENSE new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/seriesplugin/src/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/seriesplugin/src/Logger.py b/seriesplugin/src/Logger.py new file mode 100644 index 000000000..1f2b8bf9a --- /dev/null +++ b/seriesplugin/src/Logger.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +from . import _ + +import logging + +import os, sys, traceback + +from Components.config import config + +from Tools.Notifications import AddPopup +from Screens.MessageBox import MessageBox + + +log = None + + +class Logger(object): + def __init__(self): + self.local_log = "" + self.local_log_enabled = False + + self.instance = logging.getLogger("SeriesPlugin") + self.instance.setLevel(logging.DEBUG) + + self.reinit() + + def reinit(self): + self.instance.handlers = [] + + if config.plugins.seriesplugin.debug_prints.value: + shandler = logging.StreamHandler(sys.stdout) + shandler.setLevel(logging.DEBUG) + + sformatter = logging.Formatter('[%(name)s] %(levelname)s - %(message)s') + shandler.setFormatter(sformatter) + + self.instance.addHandler(shandler) + self.instance.setLevel(logging.DEBUG) + + if config.plugins.seriesplugin.write_log.value: + fhandler = logging.FileHandler(config.plugins.seriesplugin.log_file.value) + fhandler.setLevel(logging.DEBUG) + + fformatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + fhandler.setFormatter(fformatter) + + self.instance.addHandler(fhandler) + self.instance.setLevel(logging.DEBUG) + + def start(self): + # Start a temporary log, which will be removed after reading. + # Debug is not included + self.local_log = "" + self.local_log_enabled = True + + def append(self, strargs): + if self.local_log_enabled: + self.local_log += " " + strargs + + def get(self): + self.local_log_enabled = False + return self.local_log + + def shutdown(self): + if self.instance: + self.instance.shutdown() + + def success(self, *args): + strargs = " ".join( [ str(arg) for arg in args ] ) + + self.append(strargs) + + if self.instance: + self.instance.info(strargs) + + elif config.plugins.seriesplugin.debug_prints.value: + print strargs + + if int(config.plugins.seriesplugin.popups_success_timeout.value) != 0: + AddPopup( + strargs, + MessageBox.TYPE_INFO, + int(config.plugins.seriesplugin.popups_success_timeout.value), + 'SP_PopUp_ID_Success_'+strargs + ) + + def info(self, *args): + strargs = " ".join( [ str(arg) for arg in args ] ) + + self.append(strargs) + + if self.instance: + self.instance.info(strargs) + + elif config.plugins.seriesplugin.debug_prints.value: + print strargs + + def debug(self, *args): + strargs = " ".join( [ str(arg) for arg in args ] ) + + if self.instance: + self.instance.debug(strargs) + + elif config.plugins.seriesplugin.debug_prints.value: + print strargs + + if sys.exc_info()[0]: + self.instance.debug( str(sys.exc_info()[0]) ) + self.instance.debug( str(traceback.format_exc()) ) + sys.exc_clear() + + def warning(self, *args): + strargs = " ".join( [ str(arg) for arg in args ] ) + + self.append(strargs) + + if self.instance: + self.instance.warning(strargs) + + elif config.plugins.seriesplugin.debug_prints.value: + print strargs + + if int(config.plugins.seriesplugin.popups_warning_timeout.value) != 0: + AddPopup( + strargs, + MessageBox.TYPE_WARNING, + int(config.plugins.seriesplugin.popups_warning_timeout.value), + 'SP_PopUp_ID_Warning_'+strargs + ) + + def error(self, *args): + strargs = " ".join( [ str(arg) for arg in args ] ) + + self.append(strargs) + + if self.instance: + self.instance.error(strargs) + + elif config.plugins.seriesplugin.debug_prints.value: + print strargs + + AddPopup( + strargs, + MessageBox.TYPE_ERROR, + -1, + 'SP_PopUp_ID_Error_'+strargs + ) + + def exception(self, *args): + strargs = " ".join( [ str(arg) for arg in args ] ) + + self.append(strargs) + + if self.instance: + self.instance.exception(strargs) + + elif config.plugins.seriesplugin.debug_prints.value: + print strargs + + AddPopup( + strargs, + MessageBox.TYPE_ERROR, + -1, + 'SP_PopUp_ID_Exception_'+strargs + ) + + +log = Logger() diff --git a/seriesplugin/src/Logos/Fernsehserien.png b/seriesplugin/src/Logos/Fernsehserien.png new file mode 100644 index 000000000..083014bf3 Binary files /dev/null and b/seriesplugin/src/Logos/Fernsehserien.png differ diff --git a/seriesplugin/src/Logos/Makefile.am b/seriesplugin/src/Logos/Makefile.am new file mode 100644 index 000000000..5b62833de --- /dev/null +++ b/seriesplugin/src/Logos/Makefile.am @@ -0,0 +1,2 @@ +installdir = $(libdir)/enigma2/python/Plugins/Extensions/SeriesPlugin/Logos +install_DATA = Wunschliste.png Fernsehserien.png diff --git a/seriesplugin/src/Logos/Wunschliste.png b/seriesplugin/src/Logos/Wunschliste.png new file mode 100644 index 000000000..593271954 Binary files /dev/null and b/seriesplugin/src/Logos/Wunschliste.png differ diff --git a/seriesplugin/src/Makefile.am b/seriesplugin/src/Makefile.am new file mode 100644 index 000000000..e1da7241c --- /dev/null +++ b/seriesplugin/src/Makefile.am @@ -0,0 +1,4 @@ +installdir = $(libdir)/enigma2/python/Plugins/Extensions/SeriesPlugin +SUBDIRS = Identifiers Logos Images Skins +install_PYTHON = *.py +install_DATA = maintainer.info LICENSE plugin.png diff --git a/seriesplugin/src/ModuleBase.py b/seriesplugin/src/ModuleBase.py new file mode 100644 index 000000000..1a649d13d --- /dev/null +++ b/seriesplugin/src/ModuleBase.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# by betonme @2012 + + +class ModuleBase(object): + def __init__(self): + pass + + + ################################################ + # Base classmethod functions + @classmethod + def getClass(cls): + # Return the Class + return cls.__name__ + + + ################################################ + # Base functions + def getName(self, dummy=None): + # Return the Class Name + return self.__class__.__name__ diff --git a/seriesplugin/src/Modules.py b/seriesplugin/src/Modules.py new file mode 100644 index 000000000..28d757def --- /dev/null +++ b/seriesplugin/src/Modules.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=167779 +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import os, sys, traceback + +from Tools.Directories import resolveFilename, SCOPE_PLUGINS + +# Plugin framework +import imp, inspect + +# Plugin internal +from . import _ +from Logger import log + +# Constants +IDENTIFIER_PATH = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/Identifiers/" ) + + +class Modules(object): + + def __init__(self): + from IdentifierBase import IdentifierBase2 + self.modules = self.loadModules(IDENTIFIER_PATH, IdentifierBase2) + log.debug("SP Modules:", self.modules) + + ####################################################### + # Module functions + def loadModules(self, path, base): + modules = {} + + if not os.path.exists(path): + log.debug("[SP Modules]: Error: Path doesn't exist: " + path) + return + + # Import all subfolders to allow relative imports + for root, dirs, files in os.walk(path): + if root not in sys.path: + sys.path.append(root) + + # List files + files = [fname[:-3] for fname in os.listdir(path) if fname.endswith(".py") and not fname.startswith("__")] + log.debug(files) + if not files: + files = [fname[:-4] for fname in os.listdir(path) if fname.endswith(".pyo")] + log.debug(files) + + # Import modules + for name in files: + module = None + + if name == "__init__": + continue + + try: + fp, pathname, description = imp.find_module(name, [path]) + except Exception as e: + log.debug("[SP Modules] Find module exception: " + str(e)) + fp = None + + if not fp: + log.debug("[SP Modules] No module found: " + str(name)) + continue + + try: + module = imp.load_module( name, fp, pathname, description) + except Exception as e: + log.debug("[SP Modules] Load exception: " + str(e)) + finally: + # Since we may exit via an exception, close fp explicitly. + if fp: fp.close() + + if not module: + log.debug("[SP Modules] No module available: " + str(name)) + continue + + # Continue only if the attribute is available + if not hasattr(module, name): + log.debug("[SP Modules] Warning attribute not available: " + str(name)) + continue + + # Continue only if attr is a class + attr = getattr(module, name) + if not inspect.isclass(attr): + log.debug("[SeriesService] Warning no class definition: " + str(name)) + continue + + # Continue only if the class is a subclass of the corresponding base class + if not issubclass( attr, base): + log.debug("[SP Modules] Warning no subclass of base: " + str(name)) + continue + + # Add module to the module list + modules[name] = attr + return modules + + def instantiateModuleWithName(self, name): + if self.modules: + module = self.modules.get(name) + if module and callable(module): + # Create instance + try: + return module() + except Exception as e: + log.exception("[SeriesService] Instantiate exception: " + str(module) + "\n" + str(e)) + if sys.exc_info()[0]: + log.debug("Unexpected error: ", sys.exc_info()[0]) + traceback.print_exc(file=sys.stdout) + return None + else: + log.debug("[SeriesService] Module is not callable: " + str(name)) + return None + else: + log.debug("[SeriesService] No modules for name: " + str(name)) + return None + + def instantiateModule(self, module): + if module and callable(module): + # Create instance + try: + return module() + except Exception as e: + log.exception("[SeriesService] Instantiate exception: " + str(module) + "\n" + str(e)) + if sys.exc_info()[0]: + log.debug("Unexpected error: ", sys.exc_info()[0]) + traceback.print_exc(file=sys.stdout) + return None + else: + log.debug("[SeriesService] Module is not callable: " + str(module.getClass())) + return None diff --git a/seriesplugin/src/OrderedDict.py b/seriesplugin/src/OrderedDict.py new file mode 100644 index 000000000..281b163c2 --- /dev/null +++ b/seriesplugin/src/OrderedDict.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy. +# Passes Python2.7's test suite and incorporates all the latest updates. + +try: + from thread import get_ident as _get_ident +except ImportError: + from dummy_thread import get_ident as _get_ident + +try: + from _abcoll import KeysView, ValuesView, ItemsView +except ImportError: + pass + + +class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as for regular dictionaries. + + # The internal self.__map dictionary maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # Each link is stored as a list of length three: [PREV, NEXT, KEY]. + + def __init__(self, *args, **kwds): + '''Initialize an ordered dictionary. Signature is the same as for + regular dictionaries, but keyword arguments are not recommended + because their insertion order is arbitrary. + + ''' + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__root = root = [] # sentinel node + root[:] = [root, root, None] + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, dict_setitem=dict.__setitem__): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link which goes at the end of the linked + # list, and the inherited dictionary is updated with the new key/value pair. + if key not in self: + root = self.__root + last = root[0] + last[1] = root[0] = self.__map[key] = [last, root, key] + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which is + # then removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link_prev, link_next, key = self.__map.pop(key) + link_prev[1] = link_next + link_next[0] = link_prev + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + root = self.__root + curr = root[1] + while curr is not root: + yield curr[2] + curr = curr[1] + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + root = self.__root + curr = root[0] + while curr is not root: + yield curr[2] + curr = curr[0] + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + try: + for node in self.__map.itervalues(): + del node[:] + root = self.__root + root[:] = [root, root, None] + self.__map.clear() + except AttributeError: + pass + dict.clear(self) + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root[0] + link_prev = link[0] + link_prev[1] = root + root[0] = link_prev + else: + link = root[1] + link_next = link[1] + root[1] = link_next + link_next[0] = root + key = link[2] + del self.__map[key] + value = dict.pop(self, key) + return key, value + + # -- the following methods do not depend on the internal structure -- + + def keys(self): + 'od.keys() -> list of keys in od' + return list(self) + + def values(self): + 'od.values() -> list of values in od' + return [self[key] for key in self] + + def items(self): + 'od.items() -> list of (key, value) pairs in od' + return [(key, self[key]) for key in self] + + def iterkeys(self): + 'od.iterkeys() -> an iterator over the keys in od' + return iter(self) + + def itervalues(self): + 'od.itervalues -> an iterator over the values in od' + for k in self: + yield self[k] + + def iteritems(self): + 'od.iteritems -> an iterator over the (key, value) items in od' + for k in self: + yield (k, self[k]) + + def update(*args, **kwds): + '''od.update(E, **F) -> None. Update od from dict/iterable E and F. + + If E is a dict instance, does: for k in E: od[k] = E[k] + If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] + Or if E is an iterable of items, does: for k, v in E: od[k] = v + In either case, this is followed by: for k, v in F.items(): od[k] = v + + ''' + if len(args) > 2: + raise TypeError('update() takes at most 2 positional ' + 'arguments (%d given)' % (len(args),)) + elif not args: + raise TypeError('update() takes at least 1 argument (0 given)') + self = args[0] + # Make progressively weaker assumptions about "other" + other = () + if len(args) == 2: + other = args[1] + if isinstance(other, dict): + for key in other: + self[key] = other[key] + elif hasattr(other, 'keys'): + for key in other.keys(): + self[key] = other[key] + else: + for key, value in other: + self[key] = value + for key, value in kwds.items(): + self[key] = value + + __update = update # let subclasses override update without breaking __init__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value. + If key is not found, d is returned if given, otherwise KeyError is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + def __repr__(self, _repr_running={}): + 'od.__repr__() <==> repr(od)' + call_key = id(self), _get_ident() + if call_key in _repr_running: + return '...' + _repr_running[call_key] = 1 + try: + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + finally: + del _repr_running[call_key] + + def __reduce__(self): + 'Return state information for pickling' + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S + and values equal to v (which defaults to None). + + ''' + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return len(self)==len(other) and self.items() == other.items() + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other + + # -- the following methods are only used in Python 2.7 -- + + def viewkeys(self): + "od.viewkeys() -> a set-like object providing a view on od's keys" + return KeysView(self) + + def viewvalues(self): + "od.viewvalues() -> an object providing a view on od's values" + return ValuesView(self) + + def viewitems(self): + "od.viewitems() -> a set-like object providing a view on od's items" + return ItemsView(self) diff --git a/seriesplugin/src/SeriesPlugin.py b/seriesplugin/src/SeriesPlugin.py new file mode 100644 index 000000000..322786d38 --- /dev/null +++ b/seriesplugin/src/SeriesPlugin.py @@ -0,0 +1,504 @@ +# -*- coding: utf-8 -*- +# by betonme @2012 + +import re + +import os, sys, traceback + +from time import localtime, strftime +from datetime import datetime + +# Localization +from . import _ + +from datetime import datetime + +from Components.config import config + +from enigma import eServiceReference, iServiceInformation, eServiceCenter, ePythonMessagePump +from ServiceReference import ServiceReference + +# Plugin framework +from Modules import Modules + +# Tools +from Tools.BoundFunction import boundFunction +from Tools.Directories import resolveFilename, SCOPE_PLUGINS +from Tools.Notifications import AddPopup +from Screens.MessageBox import MessageBox + +# Plugin internal +from Logger import log +from Channels import ChannelsBase +from XMLTVBase import XMLTVBase +from ThreadQueue import ThreadQueue +from threading import Thread, currentThread, _get_ident +#from enigma import ePythonMessagePump + + +try: + if(config.plugins.autotimer.timeout.value == 1): + config.plugins.autotimer.timeout.value = 5 + config.plugins.autotimer.save() +except Exception as e: + pass + + +# Constants +AUTOTIMER_PATH = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/AutoTimer/" ) +SERIESPLUGIN_PATH = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/" ) + +# Globals +instance = None + +CompiledRegexpNonDecimal = re.compile(r'[^\d]') +CompiledRegexpReplaceChars = None +CompiledRegexpReplaceDirChars = re.compile('[^/\wäöüß\-_\. ]') + +def dump(obj): + for attr in dir(obj): + log.debug( " %s = %s" % (attr, getattr(obj, attr)) ) + + +def getInstance(): + global instance + + if instance is None: + + log.reinit() + + from plugin import VERSION + + log.debug(" SERIESPLUGIN NEW INSTANCE " + VERSION) + log.debug( " ", strftime("%a, %d %b %Y %H:%M:%S", localtime()) ) + + try: + from Tools.HardwareInfo import HardwareInfo + log.debug( " DeviceName " + HardwareInfo().get_device_name().strip() ) + except: + sys.exc_clear() + + try: + from Components.About import about + log.debug( " EnigmaVersion " + about.getEnigmaVersionString().strip() ) + log.debug( " ImageVersion " + about.getVersionString().strip() ) + except: + sys.exc_clear() + + try: + #http://stackoverflow.com/questions/1904394/python-selecting-to-read-the-first-line-only + log.debug( " dreamboxmodel " + open("/proc/stb/info/model").readline().strip() ) + log.debug( " imageversion " + open("/etc/image-version").readline().strip() ) + log.debug( " imageissue " + open("/etc/issue.net").readline().strip() ) + except: + sys.exc_clear() + + try: + for key, value in config.plugins.seriesplugin.dict().iteritems(): + log.debug( " config..%s = %s" % (key, str(value.value)) ) + except Exception as e: + sys.exc_clear() + + global CompiledRegexpReplaceChars + try: + if config.plugins.seriesplugin.replace_chars.value: + CompiledRegexpReplaceChars = re.compile('['+config.plugins.seriesplugin.replace_chars.value.replace("\\", "\\\\\\\\")+']') + except: + log.exception( " Config option 'Replace Chars' is no valid regular expression" ) + CompiledRegexpReplaceChars = re.compile("[:\!/\\,\(\)'\?]") + + # Check autotimer + try: + from Plugins.Extensions.AutoTimer.plugin import autotimer + deprecated = False + try: + from Plugins.Extensions.AutoTimer.plugin import AUTOTIMER_VERSION + if int(AUTOTIMER_VERSION[0]) < 4: + deprecated = True + except ImportError: + AUTOTIMER_VERSION = "deprecated" + deprecated = True + log.debug( " AutoTimer: " + AUTOTIMER_VERSION ) + if deprecated: + log.warning( _("Your autotimer is deprecated") + "\n" +_("Please update it") ) + except ImportError: + log.debug( " AutoTimer: Not found" ) + + # Check dependencies + start = True + from imp import find_module + dependencies = ["difflib", "json", "re", "xml", "xmlrpclib"] + for dependency in dependencies: + try: + find_module(dependency) + except ImportError: + start = False + log.error( _("Error missing dependency") + "\n" + "python-"+dependency + "\n\n" +_("Please install missing python paket manually") ) + if start: + instance = SeriesPlugin() + + return instance + +def stopWorker(): + global instance + if instance is not None: + log.debug(" SERIESPLUGIN STOP WORKER") + instance.stop() + +def resetInstance(): + if config.plugins.seriesplugin.lookup_counter.isChanged(): + config.plugins.seriesplugin.lookup_counter.save() + + global instance + if instance is not None: + log.debug(" SERIESPLUGIN INSTANCE STOP") + instance.stop() + instance = None + + from Cacher import clearCache + clearCache() + + +def refactorTitle(org_, data): + if CompiledRegexpReplaceChars: + org = CompiledRegexpReplaceChars.sub('', org_) + log.debug(" refactor title org", org_, org) + else: + org = org_ + if data: + if config.plugins.seriesplugin.pattern_title.value and not config.plugins.seriesplugin.pattern_title.value == "Off" and not config.plugins.seriesplugin.pattern_title.value == "Disabled": + data["org"] = org + cust_ = config.plugins.seriesplugin.pattern_title.value.strip().format( **data ) + cust = cust_.replace('&','&').replace(''',"'").replace('>','>').replace('<','<').replace('"','"').replace(' ',' ') + log.debug(" refactor title", cust_, cust) + return cust + else: + return org + else: + return org + +def refactorDescription(org_, data): + if CompiledRegexpReplaceChars: + org = CompiledRegexpReplaceChars.sub('', org_) + log.debug(" refactor desc", org_, org) + else: + org = org_ + if data: + if config.plugins.seriesplugin.pattern_description.value and not config.plugins.seriesplugin.pattern_description.value == "Off" and not config.plugins.seriesplugin.pattern_description.value == "Disabled": + data["org"] = org + cust_ = config.plugins.seriesplugin.pattern_description.value.strip().format( **data ) + cust = cust_.replace("\n", " ").replace('&','&').replace(''',"'").replace('>','>').replace('<','<').replace('"','"').replace(' ',' ') + log.debug(" refactor desc", cust_, cust) + return cust + else: + return org + else: + return org + +def refactorDirectory(org, data): + dir = org + if data: + if config.plugins.seriesplugin.pattern_directory.value and not config.plugins.seriesplugin.pattern_directory.value == "Off" and not config.plugins.seriesplugin.pattern_directory.value == "Disabled": + data["org"] = org + cust_ = config.plugins.seriesplugin.pattern_directory.value.strip().format( **data ) + cust_ = cust_.replace("\n", "").replace('&','&').replace(''',"'").replace('>','>').replace('<','<').replace('"','"').replace(" ", " ").replace("//", "/") + dir = CompiledRegexpReplaceDirChars.sub(' ', cust_) + log.debug(" refactor dir", org, cust_, dir) + if dir and not os.path.exists(dir): + try: + os.makedirs(dir) + except: + log.exception("makedirs exception", dir) + return dir + +def normalizeResult(result): + if result and isinstance(result, dict): + log.debug("normalize result") + title_ = result['title'].strip() + series_ = result['series'].strip() + season_ = result['season'] + episode_ = result['episode'] + + if config.plugins.seriesplugin.cut_series_title.value and " - " in series_: + series_, sub_series_title = series_.split(" - ", 1) + result['rawseason'] = season_ or config.plugins.seriesplugin.default_season.value + result['rawepisode'] = episode_ or config.plugins.seriesplugin.default_episode.value + if season_: + result['season'] = int( CompiledRegexpNonDecimal.sub('', str(season_)) or config.plugins.seriesplugin.default_season.value or "0" ) + else: + result['season'] = int(config.plugins.seriesplugin.default_season.value) or 0 + if episode_: + result['episode'] = int( CompiledRegexpNonDecimal.sub('', str(episode_)) or config.plugins.seriesplugin.default_episode.value or "0" ) + else: + result['episode'] = int(config.plugins.seriesplugin.default_episode.value) or 0 + + if CompiledRegexpReplaceChars: + title = CompiledRegexpReplaceChars.sub('', title_) + #log.debug(" normalize title", title_, title) + series = CompiledRegexpReplaceChars.sub('', series_) + #log.debug(" normalize series", series_, series) + else: + title = title_ + series = series_ + result['title'] = title + result['series'] = series + result['date'] = strftime("%d.%m.%Y", localtime(result['begin'])) + result['time'] = strftime("%H:%M:%S", localtime(result['begin'])) + return result + else: + log.debug("normalize result failed", str(result)) + return result + + +class ThreadItem: + def __init__(self, identifier = None, callback = None, name = None, begin = None, end = None, service = None): + self.identifier = identifier + self.callback = callback + self.name = name + self.begin = begin + self.end = end + self.service = service + + +class SeriesPluginWorker(Thread): + + def __init__(self, callback): + Thread.__init__(self) + self.callback = callback + self.__running = False + self.__messages = ThreadQueue() + self.__pump = ePythonMessagePump() + try: + self.__pump_recv_msg_conn = self.__pump.recv_msg.connect(self.gotThreadMsg) + except: + self.__pump.recv_msg.get().append(self.gotThreadMsg) + self.__queue = ThreadQueue() + + def empty(self): + return self.__queue.empty() + + def finished(self): + return not self.__running + + def add(self, item): + + self.__queue.push(item) + + if not self.__running: + self.__running = True + self.start() # Start blocking code in Thread + + def gotThreadMsg(self, msg=None): + + data = self.__messages.pop() + if callable(self.callback): + self.callback(data) + + def stop(self): + self.running = False + self.__queue = ThreadQueue() + try: + self.__pump.recv_msg.get().remove(self.gotThreadMsg) + except: + pass + self.__pump_recv_msg_conn = None + + def run(self): + + while not self.__queue.empty(): + + # NOTE: we have to check this here and not using the while to prevent the parser to be started on shutdown + if not self.__running: break + + log.debug('Worker is processing') + + item = self.__queue.pop() + + result = None + + try: + result = item.identifier.getEpisode( + item.name, item.begin, item.end, item.service + ) + except Exception, e: + log.debug("Worker: Exception:", str(e)) + + # Exception finish job with error + result = str(e) + + config.plugins.seriesplugin.lookup_counter.value += 1 + + self.__messages.push( (item.callback, normalizeResult(result)) ) + + self.__pump.send(0) + + log.debug(' Worker: list is emty, done') + Thread.__init__(self) + self.__running = False + + +class SeriesPlugin(Modules, ChannelsBase): + + def __init__(self): + log.debug("Main: Init") + Modules.__init__(self) + ChannelsBase.__init__(self) + + self.thread = SeriesPluginWorker(self.gotResult) + + # Because of the same XMLFile base class we intantiate a new object + self.xmltv = XMLTVBase() + + self.serviceHandler = eServiceCenter.getInstance() + + #http://bugs.python.org/issue7980 + datetime.strptime('2012-01-01', '%Y-%m-%d') + + self.identifier_elapsed = self.instantiateModuleWithName( config.plugins.seriesplugin.identifier_elapsed.value ) + #log.debug(self.identifier_elapsed) + + self.identifier_today = self.instantiateModuleWithName( config.plugins.seriesplugin.identifier_today.value ) + #log.debug(self.identifier_today) + + self.identifier_future = self.instantiateModuleWithName( config.plugins.seriesplugin.identifier_future.value ) + #log.debug(self.identifier_future) + + pattern = config.plugins.seriesplugin.pattern_title.value + pattern = pattern.replace("{org:s}", "(.+)") + pattern = re.sub('{season:?\d*d?}', '\d+', pattern) + pattern = re.sub('{episode:?\d*d?}', '\d+', pattern) + pattern = re.sub('{rawseason:s}', '.+', pattern) + pattern = re.sub('{rawseason:s}', '.+', pattern) + pattern = pattern.replace("{title:s}", ".+") + self.compiledRegexpSeries = re.compile(pattern) + + ################################################ + # Identifier functions + def getLogo(self, future=False, today=False, elapsed=False): + if elapsed: + return self.identifier_elapsed and self.identifier_elapsed.getLogo(future, today, elapsed) + elif today: + return self.identifier_today and self.identifier_today.getLogo(future, today, elapsed) + elif future: + return self.identifier_future and self.identifier_future.getLogo(future, today, elapsed) + else: + return None + + def getEpisode(self, callback, name, begin, end=None, service=None, future=False, today=False, elapsed=False, block=False, rename=False): + + if config.plugins.seriesplugin.skip_during_records.value: + try: + import NavigationInstance + if NavigationInstance.instance.RecordTimer.isRecording(): + msg = _("Skip check during running records") + "\n\n" + _("Can be configured within the setup") + log.warning( msg) + if callable(callback): + callback(msg) + return msg + except: + pass + + # Check for episode information in title + match = self.compiledRegexpSeries.match(name) + if match: + #log.debug(match.group(0)) # Entire match + #log.debug(match.group(1)) # First parenthesized subgroup + if not rename and config.plugins.seriesplugin.skip_pattern_match.value: + msg = _("Skip check because of pattern match") + "\n" + name + "\n\n" + _("Can be configured within the setup") + log.warning(msg) + if callable(callback): + callback(msg) + return msg + if match.group(1): + name = match.group(1) + + if elapsed: + identifier = self.identifier_elapsed + elif today: + identifier = self.identifier_today + elif future: + identifier = self.identifier_future + else: + identifier = self.modules and self.instantiateModule( self.modules.itervalues().next() ) + + if not identifier: + msg = _("No identifier available") + "\n\n" + _("Please check Your installation") + log.error(msg) + if callable(callback): + callback(msg) + return msg + + elif self.channelsEmpty(): + msg = _("Channels are not matched") + "\n\n" + _("Please open the channel editor (setup) and match them") + log.error(msg) + if callable(callback): + callback(msg) + return msg + + else: + # Reset title search depth on every new request + identifier.search_depth = 0; + + # Reset the knownids on every new request + identifier.knownids = [] + + try: + serviceref = service.toString() + except: + sys.exc_clear() + serviceref = str(service) + serviceref = re.sub('::.*', ':', serviceref) + + if block == False: + + self.thread.add( ThreadItem(identifier, callback, name, begin, end, serviceref) ) + + else: + + result = None + + try: + result = identifier.getEpisode( name, begin, end, serviceref ) + except Exception, e: + log.exception("Worker:", str(e)) + + # Exception finish job with error + result = str(e) + + config.plugins.seriesplugin.lookup_counter.value += 1 + + data = normalizeResult(result) + + if callable(callback): + callback(data) + + return data + + def gotResult(self, msg): + log.debug(" Main: Thread: gotResult:", msg) + callback, data = msg + if callable(callback): + callback(data) + + if (config.plugins.seriesplugin.lookup_counter.value == 10) \ + or (config.plugins.seriesplugin.lookup_counter.value == 100) \ + or (config.plugins.seriesplugin.lookup_counter.value % 1000 == 0): + from plugin import ABOUT + about = ABOUT.format( **{'lookups': config.plugins.seriesplugin.lookup_counter.value} ) + AddPopup( + about, + MessageBox.TYPE_INFO, + -1, + 'SP_PopUp_ID_About' + ) + + def stop(self): + log.debug(" Main: stop") + if self.thread: + self.thread.stop() + # NOTE: while we don't need to join the thread, we should do so in case it's currently parsing + #self.thread.join() + + self.thread = None + self.saveXML() + self.xmltv.writeXMLTVConfig() diff --git a/seriesplugin/src/SeriesPluginBare.py b/seriesplugin/src/SeriesPluginBare.py new file mode 100644 index 000000000..8b11e6d0f --- /dev/null +++ b/seriesplugin/src/SeriesPluginBare.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# by betonme @2015 + +# for localized messages +from . import _ + +from Components.config import * + +from Screens.MessageBox import MessageBox +from Tools.Notifications import AddPopup + +# Plugin internal +from SeriesPluginTimer import SeriesPluginTimer +from Logger import log + + +loop_data = [] +loop_counter = 0 + + +def bareGetEpisode(service_ref, name, begin, end, description, path, future=True, today=False, elapsed=False): + result = _("SeriesPlugin is deactivated") + if config.plugins.seriesplugin.enabled.value: + + log.start() + + log.info("Bare:", service_ref, name, begin, end, description, path, future, today, elapsed) + + from SeriesPlugin import getInstance, refactorTitle, refactorDescription, refactorDirectory + seriesPlugin = getInstance() + data = seriesPlugin.getEpisode( + None, + name, begin, end, service_ref, future, today, elapsed, block=True + ) + + global loop_counter + loop_counter += 1 + + if data and isinstance(data, dict): + name = str(refactorTitle(name, data)) + description = str(refactorDescription(description, data)) + path = refactorDirectory(path, data) + log.info("Bare: Success", name, description, path) + return (name, description, path, log.get()) + + elif data and isinstance(data, basestring): + global loop_data + msg = _("Failed: %s." % ( str( data ) )) + log.debug(msg) + loop_data.append( name + ": " + msg ) + + else: + global loop_data + msg = _("No data available") + log.debug(msg) + loop_data.append( name + ": " + msg ) + + log.info("Bare: Failed", str(data)) + return str(data) + + return result + +def bareShowResult(): + global loop_data, loop_counter + + if loop_data: + msg = "SeriesPlugin:\n" + _("Finished with errors:\n") +"\n" +"\n".join(loop_data) + log.warning(msg) + + else: + if loop_counter > 0: + msg = "SeriesPlugin:\n" + _("Lookup of %d episodes was successful") % (loop_counter) + log.success(msg) + + loop_data = [] + loop_counter = 0 diff --git a/seriesplugin/src/SeriesPluginConfiguration.py b/seriesplugin/src/SeriesPluginConfiguration.py new file mode 100644 index 000000000..4bcd0c78a --- /dev/null +++ b/seriesplugin/src/SeriesPluginConfiguration.py @@ -0,0 +1,411 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import os + + +# for localized messages +from . import _ + +# Config +from Components.config import * +from Components.ConfigList import ConfigListScreen +from Components.Button import Button +from Components.Sources.StaticText import StaticText + +from Components.ActionMap import ActionMap +from Screens.MessageBox import MessageBox +from Screens.Screen import Screen +from Screens.Setup import SetupSummary + +from Tools.Directories import resolveFilename, SCOPE_PLUGINS + +from Plugins.Plugin import PluginDescriptor + +# Plugin internal +from SeriesPlugin import resetInstance, getInstance +from SeriesPluginIndependent import startIndependent, stopIndependent +from FilePatterns import readFilePatterns +from DirectoryPatterns import readDirectoryPatterns +from Logger import log +from ShowLogScreen import ShowLogScreen +from Channels import getTVBouquets +from ChannelEditor import ChannelEditor + + +def checkList(cfg): + for choices in cfg.choices.choices: + if cfg.value == choices[0]: + return + for choices in cfg.choices.choices: + if cfg.default == choices[0]: + cfg.value = cfg.default + return + cfg.value = cfg.choices.choices[0][0] + + +####################################################### +# Configuration screen +class SeriesPluginConfiguration(ConfigListScreen, Screen): + + skinfile = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/Skins/Setup.xml" ) + skin = open(skinfile).read() + + def __init__(self, session): + Screen.__init__(self, session) + self.skinName = [ "SeriesPluginConfiguration" ] + + from plugin import NAME, VERSION + self.setup_title = NAME + " " + _("Configuration") + " " + VERSION + + log.debug("SeriesPluginConfiguration") + + self.onChangedEntry = [ ] + + # Buttons + self["key_red"] = Button(_("Cancel")) + self["key_green"] = Button(_("OK")) + self["key_blue"] = Button(_("Show Log")) + self["key_yellow"] = Button(_("Channel Edit")) + + # Define Actions + self["actions"] = ActionMap(["SetupActions", "ChannelSelectBaseActions", "ColorActions"], + { + "cancel": self.keyCancel, + "save": self.keySave, + "nextBouquet": self.pageUp, + "prevBouquet": self.pageDown, + "blue": self.showLog, + "yellow": self.openChannelEditor, + "ok": self.keyOK, + "left": self.keyLeft, + "right": self.keyRight, + }, -2) # higher priority + + stopIndependent() + #resetInstance() + self.seriesPlugin = getInstance() + + # Create temporary identifier config elements + identifiers = self.seriesPlugin.modules + identifiers_elapsed = [k for k,v in identifiers.items() if v.knowsElapsed()] + identifiers_today = [k for k,v in identifiers.items() if v.knowsToday()] + identifiers_future = [k for k,v in identifiers.items() if v.knowsFuture()] + if config.plugins.seriesplugin.identifier_elapsed.value in identifiers_elapsed: + self.cfg_identifier_elapsed = NoSave( ConfigSelection(choices = identifiers_elapsed, default = config.plugins.seriesplugin.identifier_elapsed.value) ) + else: + self.cfg_identifier_elapsed = NoSave( ConfigSelection(choices = identifiers_elapsed, default = identifiers_elapsed[0]) ) + self.changesMade = True + if config.plugins.seriesplugin.identifier_today.value in identifiers_today: + self.cfg_identifier_today = NoSave( ConfigSelection(choices = identifiers_today, default = config.plugins.seriesplugin.identifier_today.value) ) + else: + self.cfg_identifier_today = NoSave( ConfigSelection(choices = identifiers_today, default = identifiers_today[0]) ) + self.changesMade = True + if config.plugins.seriesplugin.identifier_future.value in identifiers_future: + self.cfg_identifier_future = NoSave( ConfigSelection(choices = identifiers_future, default = config.plugins.seriesplugin.identifier_future.value) ) + else: + self.cfg_identifier_future = NoSave( ConfigSelection(choices = identifiers_future, default = identifiers_future[0]) ) + self.changesMade = True + + # Load patterns + patterns_file = readFilePatterns() + self.cfg_pattern_title = NoSave( ConfigSelection(choices = patterns_file, default = config.plugins.seriesplugin.pattern_title.value ) ) + self.cfg_pattern_description = NoSave( ConfigSelection(choices = patterns_file, default = config.plugins.seriesplugin.pattern_description.value ) ) + #self.cfg_pattern_record = NoSave( ConfigSelection(choices = patterns_file, default = config.plugins.seriesplugin.pattern_record.value ) ) + patterns_directory = readDirectoryPatterns() + self.cfg_pattern_directory = NoSave( ConfigSelection(choices = patterns_directory, default = config.plugins.seriesplugin.pattern_directory.value ) ) + + bouquetList = [("", "")] + tvbouquets = getTVBouquets() + for bouquet in tvbouquets: + bouquetList.append((bouquet[1], bouquet[1])) + self.cfg_bouquet_main = NoSave( ConfigSelection(choices = bouquetList, default = config.plugins.seriesplugin.bouquet_main.value or str(list(zip(*bouquetList)[1])) ) ) + + checkList( self.cfg_pattern_title ) + checkList( self.cfg_pattern_description ) + checkList( self.cfg_pattern_directory ) + checkList( self.cfg_bouquet_main ) + + self.changesMade = False + + # Initialize Configuration + self.list = [] + self.buildConfig() + ConfigListScreen.__init__(self, self.list, session = session, on_change = self.changed) + + self.changed() + self.onLayoutFinish.append(self.layoutFinished) + + def layoutFinished(self): + self.setTitle(_(self.setup_title)) + + def buildConfig(self): + # _config list entry + # _ , config element + + self.list.append( getConfigListEntry( _("Enable SeriesPlugin") , config.plugins.seriesplugin.enabled ) ) + + if config.plugins.seriesplugin.enabled.value: + + # Check if xmltvimport exists + if os.path.exists("/etc/epgimport"): + log.debug("Config: Found epgimport") + self.list.append( getConfigListEntry( _("Enable support for EPGImport") , config.plugins.seriesplugin.epgimport ) ) + elif config.plugins.seriesplugin.epgimport.value: + self.changesMade = True + config.plugins.seriesplugin.epgimport.value = False + + # Check if xmltvimport exists + if os.path.exists("/etc/xmltvimport"): + log.debug("Config: Found xmltvimport") + self.list.append( getConfigListEntry( _("Enable support for XMLTVImport") , config.plugins.seriesplugin.xmltvimport ) ) + elif config.plugins.seriesplugin.xmltvimport.value: + self.changesMade = True + config.plugins.seriesplugin.xmltvimport.value = False + + # Check if crossepg exists + if os.path.exists("/etc/crossepg"): + log.debug("Config: Found crossepg") + self.list.append( getConfigListEntry( _("Enable support for crossepg") , config.plugins.seriesplugin.crossepg ) ) + elif config.plugins.seriesplugin.crossepg.value: + self.changesMade = True + config.plugins.seriesplugin.crossepg.value = False + + self.list.append( getConfigListEntry( _("Show in info menu") , config.plugins.seriesplugin.menu_info ) ) + self.list.append( getConfigListEntry( _("Show in extensions menu") , config.plugins.seriesplugin.menu_extensions ) ) + self.list.append( getConfigListEntry( _("Show in epg menu") , config.plugins.seriesplugin.menu_epg ) ) + self.list.append( getConfigListEntry( _("Show in channel menu") , config.plugins.seriesplugin.menu_channel ) ) + self.list.append( getConfigListEntry( _("Show Info in movie list menu") , config.plugins.seriesplugin.menu_movie_info ) ) + self.list.append( getConfigListEntry( _("Show Rename in movie list menu") , config.plugins.seriesplugin.menu_movie_rename ) ) + self.list.append( getConfigListEntry( _("Check timer list from extension menu") , config.plugins.seriesplugin.check_timer_list ) ) + + #if len( config.plugins.seriesplugin.identifier_elapsed.choices ) > 1: + #self.list.append( getConfigListEntry( _("Select identifier for elapsed events") , self.cfg_identifier_elapsed ) ) + #if len( config.plugins.seriesplugin.identifier_today.choices ) > 1: + #self.list.append( getConfigListEntry( _("Select identifier for today events") , self.cfg_identifier_today ) ) + #if len( config.plugins.seriesplugin.identifier_future.choices ) > 1: + #self.list.append( getConfigListEntry( _("Select identifier for future events") , self.cfg_identifier_future ) ) + + self.list.append( getConfigListEntry( _("Record title episode pattern") , self.cfg_pattern_title ) ) + self.list.append( getConfigListEntry( _("Record description episode pattern") , self.cfg_pattern_description ) ) + + self.list.append( getConfigListEntry( "E2: "+_("Composition of the recording filenames") , config.recording.filename_composition ) ) + self.list.append( getConfigListEntry( _("Record directory pattern") , self.cfg_pattern_directory ) ) + + self.list.append( getConfigListEntry( _("Default season") , config.plugins.seriesplugin.default_season ) ) + self.list.append( getConfigListEntry( _("Default episode") , config.plugins.seriesplugin.default_episode ) ) + + self.list.append( getConfigListEntry( _("Replace special characters in title") , config.plugins.seriesplugin.replace_chars ) ) + self.list.append( getConfigListEntry( _("Cut series title on dash") , config.plugins.seriesplugin.cut_series_title ) ) + + self.list.append( getConfigListEntry( _("Main bouquet for channel editor") , self.cfg_bouquet_main ) ) + + self.list.append( getConfigListEntry( _("Rename files") , config.plugins.seriesplugin.rename_file ) ) + if config.plugins.seriesplugin.rename_file.value: + self.list.append( getConfigListEntry( _("Use legacy filenames") + " (ä to ae)" , config.plugins.seriesplugin.rename_legacy ) ) + self.list.append( getConfigListEntry( _("Append '_' if file exist") , config.plugins.seriesplugin.rename_existing_files ) ) + + self.list.append( getConfigListEntry( _("Max time drift to match episode") , config.plugins.seriesplugin.max_time_drift ) ) + self.list.append( getConfigListEntry( _("Title search depths") , config.plugins.seriesplugin.search_depths ) ) + + self.list.append( getConfigListEntry( _("Skip search if pattern matches") , config.plugins.seriesplugin.skip_pattern_match ) ) + self.list.append( getConfigListEntry( _("Skip search during records") , config.plugins.seriesplugin.skip_during_records ) ) + + self.list.append( getConfigListEntry( _("AutoTimer independent mode") , config.plugins.seriesplugin.autotimer_independent ) ) + if config.plugins.seriesplugin.autotimer_independent.value: + self.list.append( getConfigListEntry( _("Check timer every x minutes") , config.plugins.seriesplugin.independent_cycle ) ) + + self.list.append( getConfigListEntry( _("Check Timer for corresponding EPG events") , config.plugins.seriesplugin.timer_eit_check ) ) + self.list.append( getConfigListEntry( _("Add tag 'SeriesPlugin' to timer") , config.plugins.seriesplugin.timer_add_tag ) ) + + self.list.append( getConfigListEntry( _("Socket timeout") , config.plugins.seriesplugin.socket_timeout ) ) + + self.list.append( getConfigListEntry( _("Timeout for Success Popups") , config.plugins.seriesplugin.popups_success_timeout ) ) + self.list.append( getConfigListEntry( _("Timeout for Warnings Popups") , config.plugins.seriesplugin.popups_warning_timeout ) ) + + #self.list.append( getConfigListEntry( _("Use local caching") , config.plugins.seriesplugin.caching ) ) + #if config.plugins.seriesplugin.caching.value: + # self.list.append( getConfigListEntry( _("Cache expires after x hours") , config.plugins.seriesplugin.caching_expiration ) ) + + self.list.append( getConfigListEntry( _("Channel matching file") , config.plugins.seriesplugin.channel_file ) ) + self.list.append( getConfigListEntry( _("Episode pattern file") , config.plugins.seriesplugin.pattern_file ) ) + self.list.append( getConfigListEntry( _("Directory pattern file") , config.plugins.seriesplugin.pattern_file_directories ) ) + + try: + self.list.append( getConfigListEntry( "AT: "+_("Poll automatically") , config.plugins.autotimer.autopoll ) ) + self.list.append( getConfigListEntry( "AT: "+_("Startup delay (in min)") , config.plugins.autotimer.delay ) ) + self.list.append( getConfigListEntry( "AT: "+_("Poll Interval (in h)") , config.plugins.autotimer.interval ) ) + self.list.append( getConfigListEntry( "AT: "+_("Timeout (in min)") , config.plugins.autotimer.timeout ) ) + except: + pass + + self.list.append( getConfigListEntry( _("Debug: Print debug messages (Shell)") , config.plugins.seriesplugin.debug_prints ) ) + self.list.append( getConfigListEntry( _("Debug: Write Log") , config.plugins.seriesplugin.write_log ) ) + if config.plugins.seriesplugin.write_log.value: + self.list.append( getConfigListEntry( _("Debug: Log file path") , config.plugins.seriesplugin.log_file ) ) + #self.list.append( getConfigListEntry( _("Debug: Forum user name") , config.plugins.seriesplugin.log_reply_user ) ) + #self.list.append( getConfigListEntry( _("Debug: User mail address") , config.plugins.seriesplugin.log_reply_mail ) ) + + try: + self.list.append( getConfigListEntry( "AT: "+_("Send debug messages to shell") , config.plugins.autotimer.log_shell ) ) + self.list.append( getConfigListEntry( "AT: "+ _("Write debug messages into file") , config.plugins.autotimer.log_write ) ) + if config.plugins.autotimer.log_write.value: + self.list.append( getConfigListEntry( "AT: "+_("Location and name of log file") , config.plugins.autotimer.log_file ) ) + except: + pass + + try: + self.list.append( getConfigListEntry( "E2: "+_("Enable recording debug (Timer log)") , config.recording.debug ) ) + except: + pass + + def changeConfig(self): + self.list = [] + self.buildConfig() + self["config"].setList(self.list) + + def changed(self): + for x in self.onChangedEntry: + x() + current = self["config"].getCurrent()[1] + if (current == config.plugins.seriesplugin.enabled or + current == config.plugins.seriesplugin.rename_file or + current == config.plugins.seriesplugin.autotimer_independent or + current == config.plugins.seriesplugin.write_log): + self.changeConfig() + return + try: + if current == config.plugins.autotimer.log_write.value: + self.changeConfig() + return + except: + pass + + # Overwrite ConfigListScreen keySave function + def keySave(self): + self.saveAll() + + config.plugins.seriesplugin.identifier_elapsed.value = self.cfg_identifier_elapsed.value + config.plugins.seriesplugin.identifier_today.value = self.cfg_identifier_today.value + config.plugins.seriesplugin.identifier_future.value = self.cfg_identifier_future.value + config.plugins.seriesplugin.pattern_title.value = self.cfg_pattern_title.value + config.plugins.seriesplugin.pattern_description.value = self.cfg_pattern_description.value + #config.plugins.seriesplugin.pattern_record.value = self.cfg_pattern_record.value + config.plugins.seriesplugin.pattern_directory.value = self.cfg_pattern_directory.value + config.plugins.seriesplugin.bouquet_main.value = self.cfg_bouquet_main.value + config.plugins.seriesplugin.save() + + self.seriesPlugin.saveXML() + + # Set new configuration + from plugin import WHERE_EPGMENU, WHERE_CHANNELMENU, addSeriesPlugin, removeSeriesPlugin, SHOWINFO, RENAMESERIES, CHECKTIMERS, info, sp_extension, channel, movielist_info, movielist_rename, checkTimers + + if config.plugins.seriesplugin.menu_info.value: + addSeriesPlugin(PluginDescriptor.WHERE_EVENTINFO, SHOWINFO, info) + else: + removeSeriesPlugin(PluginDescriptor.WHERE_EVENTINFO, SHOWINFO) + + if config.plugins.seriesplugin.menu_extensions.value: + addSeriesPlugin(PluginDescriptor.WHERE_EXTENSIONSMENU, SHOWINFO, sp_extension) + else: + removeSeriesPlugin(PluginDescriptor.WHERE_EXTENSIONSMENU, SHOWINFO) + + if config.plugins.seriesplugin.menu_epg.value: + addSeriesPlugin(WHERE_EPGMENU, SHOWINFO) + else: + removeSeriesPlugin(WHERE_EPGMENU, SHOWINFO) + + if config.plugins.seriesplugin.menu_channel.value: + addSeriesPlugin(WHERE_CHANNELMENU, SHOWINFO, channel) + else: + removeSeriesPlugin(WHERE_CHANNELMENU, SHOWINFO) + + if config.plugins.seriesplugin.menu_movie_info.value: + addSeriesPlugin(PluginDescriptor.WHERE_MOVIELIST, SHOWINFO, movielist_info) + else: + removeSeriesPlugin(PluginDescriptor.WHERE_MOVIELIST, SHOWINFO) + + if config.plugins.seriesplugin.menu_movie_rename.value: + addSeriesPlugin(PluginDescriptor.WHERE_MOVIELIST, RENAMESERIES, movielist_rename) + else: + removeSeriesPlugin(PluginDescriptor.WHERE_MOVIELIST, RENAMESERIES) + + if config.plugins.seriesplugin.check_timer_list.value: + addSeriesPlugin(PluginDescriptor.WHERE_EXTENSIONSMENU, CHECKTIMERS, checkTimers) + else: + removeSeriesPlugin(PluginDescriptor.WHERE_EXTENSIONSMENU, CHECKTIMERS) + + # To set new module configuration + resetInstance() + + if config.plugins.seriesplugin.autotimer_independent.value: + from SeriesPluginIndependent import startIndependent + startIndependent() + + self.close() + + # Overwrite ConfigListScreen keyCancel function + def keyCancel(self): + self.help_window_was_shown = False + log.debug("SPC keyCancel") + #self.seriesPlugin.resetChannels() + resetInstance() + if self["config"].isChanged() or self.changesMade: + self.session.openWithCallback(self.cancelConfirm, MessageBox, _("Really close without saving settings?")) + else: + self.close() + + # Overwrite Screen close function + def close(self): + from plugin import ABOUT + about = ABOUT.format( **{'lookups': config.plugins.seriesplugin.lookup_counter.value} ) + self.session.openWithCallback(self.closeConfirm, MessageBox, about, MessageBox.TYPE_INFO) + + def closeConfirm(self, dummy=None): + # Call baseclass function + Screen.close(self) + + def getCurrentEntry(self): + return self["config"].getCurrent()[0] + + def getCurrentValue(self): + return str(self["config"].getCurrent()[1].getText()) + + def createSummary(self): + return SetupSummary + + def pageUp(self): + self["config"].instance.moveSelection(self["config"].instance.pageUp) + + def pageDown(self): + self["config"].instance.moveSelection(self["config"].instance.pageDown) + + def showLog(self): + #self.sendLog() + self.session.open(ShowLogScreen, config.plugins.seriesplugin.log_file.value) + + def openChannelEditor(self): + self.session.openWithCallback(self.channelEditorClosed, ChannelEditor) + + def channelEditorClosed(self, result=None): + log.debug("SPC channelEditorClosed", result) + if result: + self.changesMade = True + else: + self.seriesPlugin.resetChannels() diff --git a/seriesplugin/src/SeriesPluginIndependent.py b/seriesplugin/src/SeriesPluginIndependent.py new file mode 100644 index 000000000..a756bd676 --- /dev/null +++ b/seriesplugin/src/SeriesPluginIndependent.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +#TODO Add optional popup + +# for localized messages +from . import _ + +# Config +from Components.config import * + +import NavigationInstance +from enigma import eTimer +from time import localtime +#from ServiceReference import ServiceReference + +# Plugin internal +from SeriesPluginTimer import SeriesPluginTimer +from Logger import log + + +# Globals +instance = None + + +def startIndependent(): + global instance + instance = SeriesPluginIndependent() + return instance + +def stopIndependent(): + #Rename to closeInstance + global instance + if instance: + instance.stop() + instance = None + +def runIndependent(): + try: + + spt = SeriesPluginTimer() + + for timer in NavigationInstance.instance.RecordTimer.timer_list: + + #Maybe later + # Add a series whitelist + # Configured with a dialog + # Stored in a db or xml + + spt.getEpisode(timer) + + except Exception as e: + log.exception( _("Independent mode exception") + "\n" + str(e)) + + +####################################################### +# Label timer +class SeriesPluginIndependent(object): + + data = [] + + def __init__(self): + self.etimer = eTimer() + self.etimer_conn = None + try: + self.etimer_conn = self.etimer.timeout.connect(self.run) + except: + self.etimer.callback.append(self.run) + cycle = int(config.plugins.seriesplugin.independent_cycle.value) + if cycle > 0: + self.etimer.start( (cycle * 60 * 1000) ) + # Start timer as single shot, just for testing + #self.etimer.start( 10, True ) + + def run(self): + log.debug("SeriesPluginIndependent: run", strftime("%a, %d %b %Y %H:%M:%S", localtime()) ) + + runIndependent() + + def stop(self): + self.etimer_conn = None + try: + self.etimer.callback.remove(self.run) + except: + pass diff --git a/seriesplugin/src/SeriesPluginInfoScreen.py b/seriesplugin/src/SeriesPluginInfoScreen.py new file mode 100644 index 000000000..1ed4837d0 --- /dev/null +++ b/seriesplugin/src/SeriesPluginInfoScreen.py @@ -0,0 +1,509 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import os +import re + + +# for localized messages +from . import _ +#from time import time +from datetime import datetime + +# Config +from Components.config import config + +from Screens.Screen import Screen +from Screens.Setup import SetupSummary +from Screens.MessageBox import MessageBox +from Screens.ChannelSelection import ChannelSelectionBase + +from Components.AVSwitch import AVSwitch +from Components.ActionMap import ActionMap +from Components.Button import Button +from Components.Label import Label +from Components.ScrollLabel import ScrollLabel +from Components.Pixmap import Pixmap + +from enigma import eEPGCache, eServiceReference, eServiceCenter, iServiceInformation, ePicLoad, eServiceEvent +from ServiceReference import ServiceReference + +from RecordTimer import RecordTimerEntry, parseEvent, AFTEREVENT +from Screens.TimerEntry import TimerEntry +from Components.UsageConfig import preferredTimerPath +from Screens.TimerEdit import TimerSanityConflict + +from Tools.BoundFunction import boundFunction +from Tools.Directories import resolveFilename, SCOPE_PLUGINS + +from skin import loadSkin +from enigma import getDesktop + +# Plugin internal +from SeriesPlugin import getInstance +from Logger import log +from Channels import getChannel + +# Constants +PIXMAP_PATH = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/Logos/" ) + +instance = None + + +####################################################### +# Info screen +class SeriesPluginInfoScreen(Screen): + + desktop = getDesktop(0) + desktopSize = desktop and desktop.size() + dwidth = desktopSize and desktopSize.width() + if dwidth == 1920: + skinFile = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/Skins/InfoScreenFULLHD.xml" ) + elif dwidth == 1280: + skinFile = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/Skins/InfoScreenHD.xml" ) + elif dwidth == 1024: + skinFile = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/Skins/InfoScreenXD.xml" ) + else: + skinFile = os.path.join( resolveFilename(SCOPE_PLUGINS), "Extensions/SeriesPlugin/Skins/InfoScreenSD.xml" ) + + skin = open(skinFile).read() + + def __init__(self, session, service=None, event=None): + if session: + Screen.__init__(self, session) + + global instance + instance = self + + self.session = session + self.skinName = [ "SeriesPluginInfoScreen" ] + + self["logo"] = Pixmap() + self["cover"] = Pixmap() + self["state"] = Pixmap() + + self["event_title"] = Label() + self["event_episode"] = Label() + self["event_description"] = ScrollLabel() + self["datetime"] = Label() + self["channel"] = Label() + self["duration"] = Label() + + self["key_red"] = Button("") # Rename or Record + self["key_green"] = Button("") # Trakt Seen / Not Seen + self["key_yellow"] = Button("") # Show all Episodes of current season + self["key_blue"] = Button("") # Show all Seasons + + self.redButtonFunction = None + + #TODO HelpableActionMap + self["actions"] = ActionMap(["OkCancelActions", "EventViewActions", "DirectionActions", "ColorActions"], + { + "cancel": self.close, + "ok": self.close, + "up": self["event_description"].pageUp, + "down": self["event_description"].pageDown, + "red": self.redButton, + "prevEvent": self.prevEpisode, + "nextEvent": self.nextEpisode, + + #TODO + #"pageUp": self.pageUp, + #"pageDown": self.pageDown, + #"openSimilarList": self.openSimilarList + }) + + log.info("SeriesPluginInfo:", service, event) + self.service = service + self.event = event + + self.name = "" + self.short = "" + self.data = None + + self.path = None + self.eservice = None + + self.epg = eEPGCache.getInstance() + self.serviceHandler = eServiceCenter.getInstance() + self.seriesPlugin = getInstance() + + if session: + self.onLayoutFinish.append( self.layoutFinished ) + else: + self.getEpisode() + + def layoutFinished(self): + self.setTitle( _("SeriesPlugin Info") ) + + self.getEpisode() + + def getEpisode(self): + self.name = "" + self.short = "" + self.data = None + begin, end, duration = 0, 0, 0 + ext, channel = "", "" + + future = True + today = False + elapsed = False + + if self.service: + service = self.service + else: + service = self.service = self.session and self.session.nav.getCurrentlyPlayingServiceReference() + + ref = None + + if isinstance(service, eServiceReference): + #ref = service #Problem EPG + self.eservice = service + self.path = service.getPath() + if self.path: + # Service is a movie reference + info = self.serviceHandler.info(service) + ref = info.getInfoString(service, iServiceInformation.sServiceref) + sref = ServiceReference(ref) + ref = sref.ref + channel = sref.getServiceName() + if not channel: + ref = str(ref) + ref = re.sub('::.*', ':', ref) + sref = ServiceReference(ref) + ref = sref.ref + channel = sref.getServiceName().replace('\xc2\x86', '').replace('\xc2\x87', '') + # Get information from record meta files + self.event = info and info.getEvent(service) + future = False + today = False + elapsed = True + log.debug("eServiceReference movie", str(ref)) + else: + # Service is channel reference + ref = service + channel = ServiceReference(str(service)).getServiceName() or "" + if not channel: + try: + channel = ServiceReference(service.toString()).getServiceName() or "" + except: + pass + # Get information from event + log.debug("eServiceReference channel", str(ref)) + + elif isinstance(service, ServiceReference): + ref = service.ref + channel = service.getServiceName() + log.debug("ServiceReference", str(ref)) + + elif isinstance(service, ChannelSelectionBase): + ref = service.getCurrentSelection() + channel = ServiceReference(ref).getServiceName() or "" + log.debug("ChannelSelectionBase", str(ref)) + + # Fallbacks + if ref is None: + ref = self.session and self.session.nav.getCurrentlyPlayingServiceReference() + channel = getChannel(ref) + + log.debug("Fallback ref", ref, str(ref), channel) + + if not isinstance(self.event, eServiceEvent): + try: + self.event = ref.valid() and self.epg.lookupEventTime(ref, -1) + except: + # Maybe it is an old reference + # Has the movie been renamed earlier? + # Refresh / reload the list? + self["event_episode"].setText( "No valid selection!" ) + log.debug("No valid selection", str(ref)) + return + # Get information from epg + future = False + today = True + elapsed = False + log.debug("Fallback event", self.event) + + self.service = ref + + if self.event: + self.name = self.event.getEventName() or "" + begin = self.event.getBeginTime() or 0 + duration = self.event.getDuration() or 0 + end = begin + duration or 0 + # We got the exact margins, no need to adapt it + self.short = self.event.getShortDescription() or "" + ext = self.event.getExtendedDescription() or "" + log.debug("event") + + if not begin: + info = self.serviceHandler.info(eServiceReference(str(ref))) + #log.debug("info") + if info: + #log.debug("if info") + begin = info.getInfo(ref, iServiceInformation.sTimeCreate) or 0 + if begin: + duration = info.getLength(ref) or 0 + end = begin + duration or 0 + log.debug("sTimeCreate") + else: + end = os.path.getmtime(ref.getPath()) or 0 + duration = info.getLength(ref) or 0 + begin = end - duration or 0 + log.debug("sTimeCreate else") + elif ref: + path = ref.getPath() + #log.debug("getPath") + if path and os.path.exists(path): + begin = os.path.getmtime(path) or 0 + log.debug("getmtime") + + # We don't know the exact margins, we will assume the E2 default margins + log.debug("We don't know the exact margins, we will assume the E2 default margins") + begin = begin + (config.recording.margin_before.value * 60) + end = end - (config.recording.margin_after.value * 60) + + if self.session: + self.updateScreen(self.name, _("Retrieving Season, Episode and Title..."), self.short, ext, begin, duration, channel) + + logo = self.seriesPlugin.getLogo(future, today, elapsed) + if logo: + logopath = os.path.join(PIXMAP_PATH, logo+".png") + + if self.session and os.path.exists(logopath): + self.loadPixmap("logo", logopath ) + try: + log.debug("getEpisode:", self.name, begin, end, ref) + self.seriesPlugin.getEpisode( + self.episodeCallback, + self.name, begin, end, ref, future=future, today=today, elapsed=elapsed, block=False + ) + except Exception as e: + log.exception("exception:", str(e)) + self.episodeCallback(str(e)) + + def episodeCallback(self, data=None): + #TODO episode list handling + #store the list and just open the first one + + log.debug("episodeCallback", data) + #log.debug(data) + if data and isinstance(data, dict): + # Episode data available + self.data = data + + if data['rawseason'] == "" and data['rawepisode'] == "": + custom = _("{title:s}").format( **data ) + + elif data['rawseason'] == "": + custom = _("Episode: {rawepisode:s}\n{title:s}").format( **data ) + + elif data['rawepisode'] == "": + custom = _("Season: {rawseason:s}\n{title:s}").format( **data ) + + else: + custom = _("Season: {rawseason:s} Episode: {rawepisode:s}\n{title:s}").format( **data ) + + try: + self.setColorButtons() + except Exception as e: + # Screen already closed + log.debug("exception:", str(e)) + pass + elif data: + custom = str( data ) + else: + custom = _("No matching episode found") + + # Check if the dialog is already closed + try: + self["event_episode"].setText( custom ) + except Exception as e: + # Screen already closed + log.debug("exception:", str(e)) + pass + + + def updateScreen(self, name, episode, short, ext, begin, duration, channel): + # Adapted from EventView + self["event_title"].setText( name ) + self["event_episode"].setText( episode ) + + text = "" + if short and short != name: + text = short + if ext: + if text: + text += '\n' + text += ext + self["event_description"].setText(text) + + self["datetime"].setText( datetime.fromtimestamp(begin).strftime("%d.%m.%Y, %H:%M") ) + self["duration"].setText(_("%d min")%((duration)/60)) + self["channel"].setText(channel) + + # Handle pixmaps + def loadPixmap(self, widget, path): + sc = AVSwitch().getFramebufferScale() + size = self[widget].instance.size() + self.picload = ePicLoad() + self.picload_conn = None + try: + self.picload_conn = self.picload.PictureData.connect( boundFunction(self.loadPixmapCallback, widget) ) + except: + self.picload_conn = True + self.picload.PictureData.get().append( boundFunction(self.loadPixmapCallback, widget) ) + if self.picload and self.picload_conn: + self.picload.setPara((size.width(), size.height(), sc[0], sc[1], False, 1, "#00000000")) # Background dynamically + if self.picload.startDecode(path) != 0: + del self.picload + + def loadPixmapCallback(self, widget, picInfo=None): + if self.picload and picInfo: + ptr = self.picload.getData() + if ptr != None: + self[widget].instance.setPixmap(ptr) + self[widget].show() + del self.picload + self.picload_conn = None + + # Overwrite Screen close function + def close(self): + log.debug("user close") + + global instance + instance = None + + # Call baseclass function + Screen.close(self) + + + def setColorButtons(self): + try: + log.debug("event eit", self.event and self.event.getEventId()) + if self.service and self.data: + + if self.path and os.path.exists(self.path): + # Record file exists + self["key_red"].setText(_("Rename")) + self.redButtonFunction = self.keyRename + elif self.event and self.event.getEventId(): + # Event exists + #if (not self.service.flags & eServiceReference.isGroup) and self.service.getPath() and self.service.getPath()[0] == '/' + #for timer in self.session.nav.RecordTimer.timer_list: + # if timer.eit == eventid and timer.service_ref.ref.toString() == refstr: + # cb_func = lambda ret : not ret or self.removeTimer(timer) + self["key_red"].setText(_("Record")) + self.redButtonFunction = self.keyRecord + else: + self["key_red"].setText("") + self.redButtonFunction = None + else: + self["key_red"].setText("") + self.redButtonFunction = None + except: + # Screen already closed + log.debug("exception:", str(e)) + pass + + def redButton(self): + if callable(self.redButtonFunction): + self.redButtonFunction() + + def prevEpisode(self): + if self.service and self.data: + pass + + def nextEpisode(self): + if self.service and self.data: + pass + + def keyRename(self): + log.debug("keyRename") + ref = self.eservice + if ref and self.data: + path = ref.getPath() + if path and os.path.exists(path): + from SeriesPluginRenamer import rename + if rename(path, self.name, self.short, self.data) is True: + self["key_red"].setText("") + self.redButtonFunction = None + self.session.open( MessageBox, _("Successfully renamed"), MessageBox.TYPE_INFO ) + else: + self.session.open( MessageBox, _("Renaming failed"), MessageBox.TYPE_ERROR ) + + # Adapted from EventView + def keyRecord(self): + log.debug("keyRecord") + if self.event and self.service: + event = self.event + ref = self.service + if event is None: + return + eventid = event.getEventId() + eref = eServiceReference(str(ref)) + refstr = eref.toString() + for timer in self.session.nav.RecordTimer.timer_list: + if timer.eit == eventid and timer.service_ref.ref.toString() == refstr: + cb_func = lambda ret : not ret or self.removeTimer(timer) + self.session.openWithCallback(cb_func, MessageBox, _("Do you really want to delete %s?") % event.getEventName()) + break + else: + #newEntry = RecordTimerEntry(ServiceReference(ref), checkOldTimers = True, dirname = preferredTimerPath(), *parseEvent(self.event)) + begin, end, name, description, eit = parseEvent(self.event) + + from SeriesPlugin import refactorTitle, refactorDescription + if self.data: + name = refactorTitle(name, self.data) + description = refactorDescription(description, self.data) + + #newEntry = RecordTimerEntry(ServiceReference(refstr), begin, end, name, description, eit, dirname = preferredTimerPath()) + newEntry = RecordTimerEntry(ServiceReference(str(ref)), begin, end, name, description, eit, dirname = preferredTimerPath()) + #newEntry = RecordTimerEntry(refstr, begin, end, name, description, eit, dirname = preferredTimerPath()) + self.session.openWithCallback(self.finishedAdd, TimerEntry, newEntry) + + def removeTimer(self, timer): + log.debug("remove Timer") + timer.afterEvent = AFTEREVENT.NONE + self.session.nav.RecordTimer.removeEntry(timer) + #self["key_green"].setText(_("Add timer")) + #self.key_green_choice = self.ADD_TIMER + + def finishedAdd(self, answer): + log.debug("finished add") + if answer[0]: + entry = answer[1] + simulTimerList = self.session.nav.RecordTimer.record(entry) + if simulTimerList is not None: + for x in simulTimerList: + if x.setAutoincreaseEnd(entry): + self.session.nav.RecordTimer.timeChanged(x) + simulTimerList = self.session.nav.RecordTimer.record(entry) + if simulTimerList is not None: + self.session.openWithCallback(self.finishSanityCorrection, TimerSanityConflict, simulTimerList) + #self["key_green"].setText(_("Remove timer")) + #self.key_green_choice = self.REMOVE_TIMER + else: + #self["key_green"].setText(_("Add timer")) + #self.key_green_choice = self.ADD_TIMER + log.debug("Timeredit aborted") + + def finishSanityCorrection(self, answer): + self.finishedAdd(answer) + diff --git a/seriesplugin/src/SeriesPluginRenamer.py b/seriesplugin/src/SeriesPluginRenamer.py new file mode 100644 index 000000000..001a08a08 --- /dev/null +++ b/seriesplugin/src/SeriesPluginRenamer.py @@ -0,0 +1,322 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import os +import re +from glob import glob #Py3 ,escape + +# for localized messages +from . import _ + +# Config +from Components.config import config + +from Screens.MessageBox import MessageBox +from Tools.Notifications import AddPopup + +from Tools.BoundFunction import boundFunction +from Tools.ASCIItranslit import ASCIItranslit + +from enigma import eServiceCenter, iServiceInformation, eServiceReference +from ServiceReference import ServiceReference + +# Plugin internal +from SeriesPlugin import getInstance, refactorTitle, refactorDescription, refactorDirectory +from Logger import log + +CompiledRegexpGlobEscape = re.compile('([\[\]\?*])') # "[\\1]" + + +# By Bin4ry +def newLegacyEncode(string): + string2 = "" + for z, char in enumerate(string.decode("utf-8")): + i = ord(char) + if i < 33: + string2 += " " + elif i in ASCIItranslit: + # There is a bug in the E2 ASCIItranslit some (not all) german-umlaut(a) -> AE + if char.islower(): + string2 += ASCIItranslit[i].lower() + else: + string2 += ASCIItranslit[i] + + else: + try: + string2 += char.encode('ascii', 'strict') + except: + string2 += " " + return string2 + + +def rename(servicepath, name, short, data): + # Episode data available + log.debug("rename:", data) + result = True + + #MAYBE Check if it is already renamed? + try: + # Before renaming change content + rewriteMeta(servicepath, name, data) + except Exception as e: + log.exception("rewriteMeta:", str(e) ) + result = "rewriteMeta:" + str(e) + + if config.plugins.seriesplugin.pattern_title.value and not config.plugins.seriesplugin.pattern_title.value == "Off": + + if config.plugins.seriesplugin.rename_file.value == True: + + try: + renameFiles(servicepath, name, data) + except Exception as e: + log.exception("renameFiles:", str(e) ) + result = "renameFiles:" + str(e) + + return result + + +# Adapted from MovieRetitle setTitleDescr +def rewriteMeta(servicepath, name, data): + #TODO Use MetaSupport EitSupport classes from EMC ? + if servicepath.endswith(".ts"): + meta_file = servicepath + ".meta" + else: + meta_file = servicepath + ".ts.meta" + + # Create new meta for ts files + if not os.path.exists(meta_file): + if os.path.isfile(servicepath): + _title = os.path.basename(os.path.splitext(servicepath)[0]) + else: + _title = name + _sid = "" + _descr = "" + _time = "" + _tags = "" + metafile = open(meta_file, "w") + metafile.write("%s\n%s\n%s\n%s\n%s" % (_sid, _title, _descr, _time, _tags)) + metafile.close() + + if os.path.exists(meta_file): + metafile = open(meta_file, "r") + sid = metafile.readline() + oldtitle = metafile.readline().rstrip() + olddescr = metafile.readline().rstrip() + rest = metafile.read() + metafile.close() + + if config.plugins.seriesplugin.pattern_title.value and not config.plugins.seriesplugin.pattern_title.value == "Off": + title = refactorTitle(oldtitle, data) + else: + title = oldtitle + log.debug("title",title) + if config.plugins.seriesplugin.pattern_description.value and not config.plugins.seriesplugin.pattern_description.value == "Off": + descr = refactorDescription(olddescr, data) + else: + descr = olddescr + log.debug("descr",descr) + + metafile = open(meta_file, "w") + metafile.write("%s%s\n%s\n%s" % (sid, title, descr, rest)) + metafile.close() + return True + +def renameFiles(servicepath, name, data): + log.debug("servicepath", servicepath) + + path = os.path.dirname(servicepath) + file_name = os.path.basename(os.path.splitext(servicepath)[0]) + log.debug("file_name", file_name) + + log.debug("name ", name) + # Refactor title + name = refactorTitle(file_name, data) + log.debug("name ", name) + #if config.recording.ascii_filenames.value: + # filename = ASCIItranslit.legacyEncode(filename) + if config.plugins.seriesplugin.rename_legacy.value: + name = newLegacyEncode(name) + log.debug("name ", name) + + src = os.path.join(path, file_name) + log.debug("servicepathSrc", src) + + path = refactorDirectory(path, data) + dst = os.path.join(path, name) + log.debug("servicepathDst", dst) + + return osrename(src, dst) + +def osrename(src, dst): + #Py3 for f in glob( escape(src) + "*" ): + glob_src = CompiledRegexpGlobEscape.sub("[\\1]", src) + log.debug("glob_src ", glob_src) + for f in glob( glob_src + ".*" ): + log.debug("servicepathRnm", f) + to = f.replace(src, dst) + log.debug("servicepathTo ", to) + + if not os.path.exists(to): + try: + os.rename(f, to) + except: + log.exception("rename error", f, to) + elif config.plugins.seriesplugin.rename_existing_files.value: + log.debug("Destination file already exists", to, " - Append '_'") + return osrename( src, dst + "_") + break + else: + log.warning( _("Skipping rename because file already exists") + "\n" + to + "\n\n" + _("Can be configured within the setup") ) + return True + + +####################################################### +# Rename movies +class SeriesPluginRenamer(object): + def __init__(self, session, services, *args, **kwargs): + + log.info("SeriesPluginRenamer: services, service:", str(services)) + + if services and not isinstance(services, list): + services = [services] + + self.services = services + + self.data = [] + self.counter = 0 + + session.openWithCallback( + self.confirm, + MessageBox, + _("Do You want to start renaming?"), + MessageBox.TYPE_YESNO, + timeout = 15, + default = True + ) + + def confirm(self, confirmed): + if confirmed and self.services: + serviceHandler = eServiceCenter.getInstance() + + try: + for service in self.services: + + seriesPlugin = getInstance() + + if isinstance(service, eServiceReference): + service = service + elif isinstance(service, ServiceReference): + service = service.ref + else: + log.debug("Wrong instance") + continue + + servicepath = service.getPath() + + if not os.path.exists( servicepath ): + log.debug("File not exists: " + servicepath) + continue + + info = serviceHandler.info(service) + if not info: + log.debug("No info available: " + servicepath) + continue + + short = "" + begin = None + end = None + duration = 0 + + event = info.getEvent(service) + if event: + name = event.getEventName() or "" + short = event.getShortDescription() + begin = event.getBeginTime() + duration = event.getDuration() or 0 + end = begin + duration or 0 + # We got the exact start times, no need for margin handling + log.debug("event") + else: + name = service.getName() or info.getName(service) or "" + if name[-2:] == 'ts': + name = name[:-2] + log.debug("not event") + + if not begin: + begin = info.getInfo(service, iServiceInformation.sTimeCreate) or -1 + if begin != -1: + end = begin + (info.getLength(service) or 0) + else: + end = os.path.getmtime(servicepath) + begin = end - (info.getLength(service) or 0) + + #MAYBE we could also try to parse the filename + log.debug("We don't know the exact margins, we will assume the E2 default margins") + begin -= (int(config.recording.margin_before.value) * 60) + end += (int(config.recording.margin_after.value) * 60) + + rec_ref_str = info.getInfoString(service, iServiceInformation.sServiceref) + #channel = ServiceReference(rec_ref_str).getServiceName() + + log.debug("getEpisode:", name, begin, end, rec_ref_str) + seriesPlugin.getEpisode( + boundFunction(self.renamerCallback, servicepath, name, short), + name, begin, end, rec_ref_str, elapsed=True, block=True, rename=True + ) + + except Exception as e: + log.exception("Exception:", str(e)) + + def renamerCallback(self, servicepath, name, short, data=None): + log.debug("renamerCallback", name, data) + + result = None + + if data and isinstance(data, dict): + result = rename(servicepath, name, short, data) + + elif data and isinstance(data, basestring): + msg = _("Failed: %s." % ( str( data ) )) + log.debug(msg) + self.data.append( name + ": " + msg ) + + else: + msg = _("No data available") + log.debug(msg) + self.data.append( name + ": " + msg ) + + self.counter = self.counter +1 + + # Maybe there is a better way to avoid multiple Popups + from SeriesPlugin import getInstance + + instance = getInstance() + + if instance.thread.empty() and instance.thread.finished(): + if self.data: + msg = "SeriesPlugin:\n" + _("Record rename has been finished with %d errors:\n") % (len(self.data)) +"\n" +"\n".join(self.data) + log.warning(msg) + + else: + if self.counter > 0: + msg = "SeriesPlugin:\n" + _("%d records renamed successfully") % (self.counter) + log.success(msg) + + self.data = [] + self.counter = 0 diff --git a/seriesplugin/src/SeriesPluginTimer.py b/seriesplugin/src/SeriesPluginTimer.py new file mode 100644 index 000000000..e2a280202 --- /dev/null +++ b/seriesplugin/src/SeriesPluginTimer.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import os + +# for localized messages +from . import _ + +from time import time +from enigma import eEPGCache +from ServiceReference import ServiceReference + +# Config +from Components.config import * + +from Screens.MessageBox import MessageBox +from Tools.Notifications import AddPopup +from Tools.BoundFunction import boundFunction + +# Plugin internal +from SeriesPlugin import getInstance, refactorTitle, refactorDescription, refactorDirectory +from Logger import log + +TAG = "SeriesPlugin" + +####################################################### +# Label timer +class SeriesPluginTimer(object): + + data = [] + counter = 0; + + def __init__(self): + + log.debug("SeriesPluginTimer: New instance") + + def getEpisode(self, timer, block=False): + + log.info("timername, service, begin, end:", timer.name, str(timer.service_ref.ref), timer.begin, timer.end) + + if hasattr(timer, 'sp_in_queue'): + if timer.sp_in_queue: + msg = _("Skipping timer because it is already in queue") + log.warning(msg, timer.name) + timer.log(601, "[SeriesPlugin]" + " " + msg ) + return + + # We have to compare the length, + # because of the E2 special chars handling for creating the filenames + #if timer.name == name: + # Mad Men != Mad_Men + + if TAG in timer.tags: + msg = _("Skipping timer because it is already handled") + "\n\n" + _("Can be configured within the setup") + log.info(msg, timer.name) + timer.log(607, "[SeriesPlugin]" + " " + msg ) + return + + if timer.begin < time() + 60: + msg = _("Skipping timer because it starts in less than 60 seconds") + log.debug(msg, timer.name) + timer.log(604, "[SeriesPlugin]" + " " + msg ) + return + + if timer.isRunning(): + msg = _("Skipping timer because it is already running") + log.debug(msg, timer.name) + timer.log(605, "[SeriesPlugin]" + " " + msg ) + return + + if timer.justplay: + msg = _("Skipping timer because it is a just play timer") + log.debug(msg, timer.name) + timer.log(606, "[SeriesPlugin]" + " " + msg ) + return + + + event = None + epgcache = eEPGCache.getInstance() + + if timer.eit: + event = epgcache.lookupEventId(timer.service_ref.ref, timer.eit) + log.debug("lookupEventId", timer.eit, event) + if not(event): + event = epgcache.lookupEventTime( timer.service_ref.ref, timer.begin + ((timer.end - timer.begin) /2) ); + log.debug("lookupEventTime", event ) + + if event: + if not ( len(timer.name) == len(event.getEventName()) ): + msg = _("Skipping timer because it is already modified %s" % (timer.name) ) + log.info(msg) + timer.log(602, "[SeriesPlugin]" + " " + msg ) + return + begin = event.getBeginTime() or 0 + duration = event.getDuration() or 0 + end = begin + duration + + else: + if config.plugins.seriesplugin.timer_eit_check.value: + msg = _("Skipping timer because no event was found") + log.info(msg, timer.name) + timer.log(603, "[SeriesPlugin]" + " " + msg ) + return + else: + # We don't know the exact margins, we will assume the E2 default margins + log.debug("We don't know the exact margins, we will assume the E2 default margins") + begin = timer.begin + (config.recording.margin_before.value * 60) + end = timer.end - (config.recording.margin_after.value * 60) + + + timer.log(600, "[SeriesPlugin]" + " " + _("Try to find infos for %s" % (timer.name) ) ) + + seriesPlugin = getInstance() + + if timer.service_ref: + log.debug("getEpisode:", timer.name, timer.begin, timer.end, block) + + timer.sp_in_queue = True + + return seriesPlugin.getEpisode( + boundFunction(self.timerCallback, timer), + timer.name, begin, end, timer.service_ref, future=True, block=block + ) + else: + msg = _("Skipping lookup because no channel is specified") + log.warning(msg) + self.timerCallback(timer, msg) + return None + + def timerCallback(self, timer, data=None): + log.debug("timerCallback", data) + + if data and isinstance(data, dict) and timer: + + # Episode data available, refactor name and description + timer.name = str(refactorTitle(timer.name, data)) + timer.description = str(refactorDescription(timer.description, data)) + + timer.dirname = str(refactorDirectory(timer.dirname or config.usage.default_path.value, data)) + timer.calculateFilename() + + msg = _("Success: %s" % (timer.name)) + log.debug(msg) + timer.log(610, "[SeriesPlugin]" + " " + msg) + + if config.plugins.seriesplugin.timer_add_tag.value: + timer.tags.append(TAG) + + elif data: + msg = _("Failed: %s." % ( str( data ) )) + log.debug(msg) + timer.log(611, "[SeriesPlugin]" + " " + msg) + SeriesPluginTimer.data.append( + str(timer.name) + ": " + msg + ) + + else: + msg = _("No data available") + log.debug(msg) + timer.log(612, "[SeriesPlugin]" + " " + msg) + SeriesPluginTimer.data.append( + str(timer.name) + ": " + msg + ) + + timer.sp_in_queue = False + + SeriesPluginTimer.counter = SeriesPluginTimer.counter +1 + + # Maybe there is a better way to avoid multiple Popups + from SeriesPlugin import getInstance + + instance = getInstance() + + if instance.thread.empty() and instance.thread.finished(): + + if SeriesPluginTimer.data: + msg = "SeriesPlugin:\n" + _("Timer rename has been finished with %d errors:\n") % (len(SeriesPluginTimer.data)) +"\n" +"\n".join(SeriesPluginTimer.data) + log.warning(msg) + + else: + if SeriesPluginTimer.counter > 0: + msg = "SeriesPlugin:\n" + _("%d timer renamed successfully") % (SeriesPluginTimer.counter) + log.success(msg) + + SeriesPluginTimer.data = [] + SeriesPluginTimer.counter = 0 + + return timer diff --git a/seriesplugin/src/ShowLogScreen.py b/seriesplugin/src/ShowLogScreen.py new file mode 100644 index 000000000..c5c8832e6 --- /dev/null +++ b/seriesplugin/src/ShowLogScreen.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +import os, sys, traceback + + +# Config +from Components.config import * +from Components.Sources.StaticText import StaticText + +# Screen +from Components.ActionMap import ActionMap +from Components.ScrollLabel import ScrollLabel +from enigma import eSize, ePoint, getDesktop +from Screens.Screen import Screen +from Tools.Directories import fileExists, resolveFilename, SCOPE_PLUGINS + +# Plugin internal +from . import _ + + +class ShowLogScreen(Screen): + def __init__(self, session, logFile): + Screen.__init__(self, session) + self.skinName = ["TestBox", "Console"] + title = "" + text = "" + self.logFile = logFile + + self["text"] = ScrollLabel("") + self["actions"] = ActionMap(["WizardActions", "DirectionActions", "ChannelSelectBaseActions"], + { + "ok": self.cancel, + "back": self.cancel, + "up": self["text"].pageUp, + "down": self["text"].pageDown, + "left": self["text"].pageUp, + "right": self["text"].pageDown, + "nextBouquet": self["text"].lastPage, + "prevBouquet": self.firstPage, + }, -1) + + self.onLayoutFinish.append(self.readLog) + + def cancel(self): + self.close() + + def setText(self, text): + self["text"].setText(text) + + def close(self): + Screen.close(self) + + def firstPage(self): + self["text"].long_text.move(ePoint(0,0)) + self["text"].updateScrollbar() + + def readLog(self): + + # Set title and text + title = _("Show Log file") + text = _("Reading log file...\n") + self.logFile + _("\nCancel?") + + self.setTitle(title) + self.setText(text) + + if not fileExists(self.logFile): + self.setText(_("No log file found")) + + elif not os.path.getsize(self.logFile) == 0: + file = open(self.logFile, "r") + text = file.read() + file.close() + + try: + self.setText(text) + self["text"].lastPage() + except: + pass diff --git a/seriesplugin/src/Skins/ChannelEditor.xml b/seriesplugin/src/Skins/ChannelEditor.xml new file mode 100644 index 000000000..3641c8640 --- /dev/null +++ b/seriesplugin/src/Skins/ChannelEditor.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/seriesplugin/src/Skins/InfoScreenFULLHD.xml b/seriesplugin/src/Skins/InfoScreenFULLHD.xml new file mode 100644 index 000000000..38155fb8e --- /dev/null +++ b/seriesplugin/src/Skins/InfoScreenFULLHD.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/seriesplugin/src/Skins/InfoScreenHD.xml b/seriesplugin/src/Skins/InfoScreenHD.xml new file mode 100644 index 000000000..8d5a8e293 --- /dev/null +++ b/seriesplugin/src/Skins/InfoScreenHD.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/seriesplugin/src/Skins/InfoScreenSD.xml b/seriesplugin/src/Skins/InfoScreenSD.xml new file mode 100644 index 000000000..2dd54527a --- /dev/null +++ b/seriesplugin/src/Skins/InfoScreenSD.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/seriesplugin/src/Skins/InfoScreenXD.xml b/seriesplugin/src/Skins/InfoScreenXD.xml new file mode 100644 index 000000000..0fadc0ad2 --- /dev/null +++ b/seriesplugin/src/Skins/InfoScreenXD.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/seriesplugin/src/Skins/Makefile.am b/seriesplugin/src/Skins/Makefile.am new file mode 100644 index 000000000..85a36cf9a --- /dev/null +++ b/seriesplugin/src/Skins/Makefile.am @@ -0,0 +1,2 @@ +installdir = $(libdir)/enigma2/python/Plugins/Extensions/SeriesPlugin/Skins +install_DATA = *.xml diff --git a/seriesplugin/src/Skins/Setup.xml b/seriesplugin/src/Skins/Setup.xml new file mode 100644 index 000000000..4da26ab39 --- /dev/null +++ b/seriesplugin/src/Skins/Setup.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/seriesplugin/src/ThreadQueue.py b/seriesplugin/src/ThreadQueue.py new file mode 100644 index 000000000..cb62fe34e --- /dev/null +++ b/seriesplugin/src/ThreadQueue.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from threading import Lock +from collections import deque + +class ThreadQueue: + def __init__(self): + self.__queue = deque() + self.__lock = Lock() + + def empty(self): + return not self.__queue + + def push(self, val): + lock = self.__lock + lock.acquire() + self.__queue.append(val) + lock.release() + + def pop(self): + lock = self.__lock + lock.acquire() + if self.__queue: + ret = self.__queue.popleft() + else: + ret = None + lock.release() + return ret diff --git a/seriesplugin/src/TimeoutServerProxy.py b/seriesplugin/src/TimeoutServerProxy.py new file mode 100644 index 000000000..024b41863 --- /dev/null +++ b/seriesplugin/src/TimeoutServerProxy.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# by http://stackoverflow.com/questions/372365/set-timeout-for-xmlrpclib-serverproxy + +import xmlrpclib +import socket + +from time import time + +from Components.config import config + +# Internal +from Logger import log + + +skip_expiration = 5.0 * 60 # in seconds +reduced_timeout = 3.0 # in seconds + + +class TimeoutServerProxy(xmlrpclib.ServerProxy): + def __init__(self, *args, **kwargs): + + from Plugins.Extensions.SeriesPlugin.plugin import REQUEST_PARAMETER + uri = config.plugins.seriesplugin.serienserver_url.value + REQUEST_PARAMETER + + xmlrpclib.ServerProxy.__init__(self, uri, verbose=False, *args, **kwargs) + + timeout = config.plugins.seriesplugin.socket_timeout.value + socket.setdefaulttimeout( float(timeout) ) + + self.skip = {} + + def getWebChannels(self): + result = None + try: + result = self.sp.cache.getWebChannels() + except Exception as e: + log.exception("Exception in xmlrpc: " + str(e) + ' - ' + str(result)) + return result + + def getSeasonEpisode( self, name, webChannel, unixtime, max_time_drift ): + result = None + + skipped = self.skip.get(name, None) + if skipped: + if ( time() - skipped ) < skip_expiration: + #return _("Skipped") + socket.setdefaulttimeout( reduced_timeout ) + else: + del self.skip[name] + + try: + result = self.sp.cache.getSeasonEpisode( name, webChannel, unixtime, max_time_drift ) + log.debug("SerienServer getSeasonEpisode result:", result) + except Exception as e: + msg = "Exception in xmlrpc: \n" + str(e) + ' - ' + str(result) + "\n\nfor" + name + " (" + webChannel + ")" + if not config.plugins.seriesplugin.autotimer_independent.value: + log.exception(msg) + else: + # The independant mode could have a lot of non series entries + log.debug(msg) + self.skip[name] = time() + result = str(e) + + if skipped: + timeout = config.plugins.seriesplugin.socket_timeout.value + socket.setdefaulttimeout( float(timeout) ) + + return result diff --git a/seriesplugin/src/WebChannels.py b/seriesplugin/src/WebChannels.py new file mode 100644 index 000000000..435584e77 --- /dev/null +++ b/seriesplugin/src/WebChannels.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __init__ import _ + +from Components.config import config + +# Internal +from Logger import log +from TimeoutServerProxy import TimeoutServerProxy + + +class WebChannels(object): + def __init__(self): + + self.server = TimeoutServerProxy() + + def getWebChannels(self): + + log.debug("SerienServer getWebChannels()") + + result = self.server.getWebChannels() + log.debug("SerienServer getWebChannels result:", result) + + return result diff --git a/seriesplugin/src/XMLFile.py b/seriesplugin/src/XMLFile.py new file mode 100644 index 000000000..0b09eb549 --- /dev/null +++ b/seriesplugin/src/XMLFile.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +####################################################################### +# +# Series Plugin for Enigma-2 +# Coded by betonme (c) 2012 +# Support: http://www.i-have-a-dreambox.com/wbb2/thread.php?threadid=TBD +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +####################################################################### + +import os +import re + +# Config +from Components.config import config + + +# XML +from xml.etree.cElementTree import ElementTree, parse, Element, SubElement, Comment +from Tools.XMLTools import stringToXML + +# Plugin internal +from . import _ +from Logger import log + +def indent(elem, level=0): + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +class XMLFile(object): + def __init__(self, path): + self.__cache = "" + self.__mtime = -1 + self.__path = path + + def getPath(self): + return self.__path + + def setPath(self, path): + self.__path = path + + def readXML(self): + + path = self.__path + log.debug("Read XML from " + str(path)) + + if not path: + log.debug("No configuration file given") + return None + + # Abort if no config found + if not os.path.exists(path): + log.debug("Configuration file does not exist") + return None + + # Parse if mtime differs from whats saved + mtime = os.path.getmtime(path) + if mtime == self.__mtime: + # No changes in configuration, won't read again + return self.__cache + + # Parse XML + try: + etree = parse(path) + except Exception as e: + log.exception("Exception in read XML: " + str(e)) + etree = None + mtime = -1 + + # Save time and cache file content + self.__mtime = mtime + self.__cache = etree + return self.__cache + + def writeXML(self, etree): + + path = self.__path + log.debug("Write XML to " + path) + + try: + etree.write(path, encoding='utf-8', xml_declaration=True) + except Exception as e: + log.exception("Exception in write XML: " + str(e)) + etree = None + mtime = -1 + + # Save time and cache file content + self.__mtime = os.path.getmtime( path ) + self.__cache = etree diff --git a/seriesplugin/src/XMLTVBase.py b/seriesplugin/src/XMLTVBase.py new file mode 100644 index 000000000..7a89f4b64 --- /dev/null +++ b/seriesplugin/src/XMLTVBase.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# by betonme @2015 + +import os +import re + +# Config +from Components.config import config + +# XML +from xml.etree.cElementTree import ElementTree, parse, Element, SubElement, Comment +from Tools.XMLTools import stringToXML + +# Plugin internal +from . import _ +from XMLFile import XMLFile, indent +from Logger import log + + +class XMLTVBase(object): + + def __init__(self): + + self.epgimport = None + self.epgimportversion = "0" + self.xmltvimport = None + self.xmltvimportversion = "0" + self.crossepg = None + self.crossepgversion = "0" + + # Check if xmltvimport exists + if os.path.exists("/etc/epgimport"): + log.debug("readXMLTV: Found epgimport") + path = "/etc/epgimport/wunschliste.sources.xml" + self.epgimport = XMLFile(path) + + # Check if xmltvimport exists + if os.path.exists("/etc/xmltvimport"): + log.debug("readXMLTV: Found xmltvimport") + path = "/etc/xmltvimport/wunschliste.sources.xml" + self.xmltvimport = XMLFile(path) + + # Check if crossepg exists + if os.path.exists("/etc/crossepg"): + log.debug("readXMLTV: Found crossepg") + path = "/etc/crossepg/wunschliste.sources.xml" + self.crossepg = XMLFile(path) + + self.readXMLTVConfig() + + def readXMLTVConfig(self): + + if self.epgimport: + etree = self.epgimport.readXML() + if etree: + self.epgimportversion = etree.getroot().get("version", "1") + log.debug("readXMLTVConfig: EPGImport Version " + self.epgimportversion) + + if self.xmltvimport: + etree = self.xmltvimport.readXML() + if etree: + self.xmltvimportversion = etree.getroot().get("version", "1") + log.debug("readXMLTVConfig: XMLTVImport Version " + self.xmltvimportversion) + + if self.crossepg: + etree = self.crossepg.readXML() + if etree: + self.crossepgversion = etree.getroot().get("version", "1") + log.debug("readXMLTVConfig: crossepg Version " + self.crossepgversion) + + def writeXMLTVConfig(self): + + if self.epgimport is None and self.xmltvimport is None and self.crossepg is None: + return + + if int(self.epgimportversion[0]) >= 5 and int(self.xmltvimportversion[0]) >= 5 and int(self.crossepgversion[0]) >= 5: + return; + + if config.plugins.seriesplugin.epgimport.value == False and config.plugins.seriesplugin.xmltvimport.value == False and config.plugins.seriesplugin.crossepg.value == False: + return + + # Build Header + from plugin import NAME, VERSION + root = Element("sources") + root.set('version', VERSION) + root.set('created_by', NAME) + root.append(Comment(_("Don't edit this manually unless you really know what you are doing"))) + + element = SubElement( root, "source", type = "gen_xmltv", channels = "wunschliste.channels.xml" ) + + SubElement( element, "description" ).text = "Wunschliste XMLTV" + SubElement( element, "url" ).text = config.plugins.seriesplugin.xmltv_url.value + + etree = ElementTree( root ) + + indent(etree.getroot()) + + if config.plugins.seriesplugin.epgimport.value: + log.debug("Write: xml channels for epgimport") + if self.epgimport: + try: + self.epgimport.writeXML( etree ) + except Exception as e: + log.exception("Exception in write XML: " + str(e)) + + if config.plugins.seriesplugin.xmltvimport.value: + log.debug("Write: xml channels for xmltvimport") + if self.xmltvimport: + try: + self.xmltvimport.writeXML( etree ) + except Exception as e: + log.exception("Exception in write XML: " + str(e)) + + if config.plugins.seriesplugin.crossepg.value: + log.debug("Write: xml channels for crossepg") + if self.crossepg: + try: + self.crossepg.writeXML( etree ) + except Exception as e: + log.exception("Exception in write XML: " + str(e)) diff --git a/seriesplugin/src/__init__.py b/seriesplugin/src/__init__.py new file mode 100644 index 000000000..51d9d8f8b --- /dev/null +++ b/seriesplugin/src/__init__.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +from Components.config import config, ConfigSubsection, ConfigOnOff, ConfigNumber, ConfigSelection, ConfigYesNo, ConfigText, ConfigSelectionNumber +from Components.Language import language +from Tools.Directories import resolveFilename, SCOPE_PLUGINS, SCOPE_LANGUAGE +from os import environ as os_environ +import gettext + + +####################################################### +# Initialize Configuration +config.plugins.seriesplugin = ConfigSubsection() + +config.plugins.seriesplugin.enabled = ConfigOnOff(default = False) + +config.plugins.seriesplugin.epgimport = ConfigYesNo(default = False) +config.plugins.seriesplugin.xmltvimport = ConfigYesNo(default = False) +config.plugins.seriesplugin.crossepg = ConfigYesNo(default = False) + +config.plugins.seriesplugin.menu_info = ConfigYesNo(default = True) +config.plugins.seriesplugin.menu_extensions = ConfigYesNo(default = False) +config.plugins.seriesplugin.menu_epg = ConfigYesNo(default = False) +config.plugins.seriesplugin.menu_channel = ConfigYesNo(default = True) +config.plugins.seriesplugin.menu_movie_info = ConfigYesNo(default = True) +config.plugins.seriesplugin.menu_movie_rename = ConfigYesNo(default = True) + +config.plugins.seriesplugin.identifier_elapsed = ConfigText(default = "", fixed_size = False) +config.plugins.seriesplugin.identifier_today = ConfigText(default = "", fixed_size = False) +config.plugins.seriesplugin.identifier_future = ConfigText(default = "", fixed_size = False) + +#config.plugins.seriesplugin.manager = ConfigSelection(choices = [("", "")], default = "") +#config.plugins.seriesplugin.guide = ConfigSelection(choices = [("", "")], default = "") + +config.plugins.seriesplugin.pattern_file = ConfigText(default = "/etc/enigma2/seriesplugin_patterns.json", fixed_size = False) +config.plugins.seriesplugin.pattern_title = ConfigText(default = "{org:s} S{season:02d}E{episode:02d} {title:s}", fixed_size = False) +config.plugins.seriesplugin.pattern_description = ConfigText(default = "S{season:02d}E{episode:02d} {title:s} {org:s}", fixed_size = False) +#config.plugins.seriesplugin.pattern_record = ConfigText(default = "{org:s} S{season:02d}E{episode:02d} {title:s}", fixed_size = False) +config.plugins.seriesplugin.pattern_file_directories = ConfigText(default = "/etc/enigma2/seriesplugin_pattern_directories.json", fixed_size = False) +config.plugins.seriesplugin.pattern_directory = ConfigText(default = "Disabled", fixed_size = False) + +config.plugins.seriesplugin.default_season = ConfigSelectionNumber(0, 1, 1, default = 1) +config.plugins.seriesplugin.default_episode = ConfigSelectionNumber(0, 1, 1, default = 1) + +config.plugins.seriesplugin.replace_chars = ConfigText(default = ":\!\\,\(\)'\?", fixed_size = False) +config.plugins.seriesplugin.cut_series_title = ConfigYesNo(default = False) + +config.plugins.seriesplugin.channel_file = ConfigText(default = "/etc/enigma2/seriesplugin_channels.xml", fixed_size = False) + +config.plugins.seriesplugin.bouquet_main = ConfigText(default = "", fixed_size = False) + +config.plugins.seriesplugin.rename_file = ConfigYesNo(default = True) +config.plugins.seriesplugin.rename_legacy = ConfigYesNo(default = False) +config.plugins.seriesplugin.rename_existing_files = ConfigYesNo(default = False) + +config.plugins.seriesplugin.max_time_drift = ConfigSelectionNumber(0, 600, 1, default = 15) +config.plugins.seriesplugin.search_depths = ConfigSelectionNumber(0, 10, 1, default = 0) + +config.plugins.seriesplugin.skip_during_records = ConfigYesNo(default=False) +config.plugins.seriesplugin.skip_pattern_match = ConfigYesNo(default=True) + +config.plugins.seriesplugin.autotimer_independent = ConfigYesNo(default = False) +config.plugins.seriesplugin.independent_cycle = ConfigSelectionNumber(5, 24*60, 5, default = 60) + +config.plugins.seriesplugin.check_timer_list = ConfigYesNo(default = False) + +config.plugins.seriesplugin.timer_eit_check = ConfigYesNo(default = True) +config.plugins.seriesplugin.timer_add_tag = ConfigYesNo(default = True) + +config.plugins.seriesplugin.socket_timeout = ConfigSelectionNumber(0, 600, 1, default = 10) + +config.plugins.seriesplugin.popups_success_timeout = ConfigSelectionNumber(-1, 20, 1, default = 3) +config.plugins.seriesplugin.popups_warning_timeout = ConfigSelectionNumber(-1, 20, 1, default = -1) + +config.plugins.seriesplugin.caching = ConfigYesNo(default = True) +config.plugins.seriesplugin.caching_expiration = ConfigSelectionNumber(0, 48, 1, default = 6) + +config.plugins.seriesplugin.debug_prints = ConfigYesNo(default = False) +config.plugins.seriesplugin.write_log = ConfigYesNo(default = False) +config.plugins.seriesplugin.log_file = ConfigText(default = "/tmp/seriesplugin.log", fixed_size = False) +config.plugins.seriesplugin.log_reply_user = ConfigText(default = "Dreambox User", fixed_size = False) +config.plugins.seriesplugin.log_reply_mail = ConfigText(default = "myemail@home.com", fixed_size = False) + +# Internal +config.plugins.seriesplugin.lookup_counter = ConfigNumber(default = 0) +#config.plugins.seriesplugin.uid = ConfigText(default = str(time()), fixed_size = False) + +config.plugins.seriesplugin.proxy_url = ConfigText(default = 'http://www.serienserver.de/proxy/proxy.php', fixed_size = False) +config.plugins.seriesplugin.serienserver_url = ConfigText(default = 'http://www.serienserver.de/cache/cache.php', fixed_size = False) +config.plugins.seriesplugin.xmltv_url = ConfigText(default = 'http://www.serienserver.de/xmltv/wunschliste.xml', fixed_size = False) + + +def localeInit(): + lang = language.getLanguage()[:2] # getLanguage returns e.g. "fi_FI" for "language_country" + os_environ["LANGUAGE"] = lang # Enigma doesn't set this (or LC_ALL, LC_MESSAGES, LANG). gettext needs it! + gettext.bindtextdomain("SeriesPlugin", resolveFilename(SCOPE_PLUGINS, "Extensions/SeriesPlugin/locale")) + +_ = lambda txt: gettext.dgettext("SeriesPlugin", txt) if txt else "" + +localeInit() +language.addCallback(localeInit) diff --git a/seriesplugin/src/maintainer.info b/seriesplugin/src/maintainer.info new file mode 100644 index 000000000..5a72a19fd --- /dev/null +++ b/seriesplugin/src/maintainer.info @@ -0,0 +1,2 @@ +glaserfrank(at)gmail.com +SeriesPlugin diff --git a/seriesplugin/src/plugin.png b/seriesplugin/src/plugin.png new file mode 100644 index 000000000..a119e7de4 Binary files /dev/null and b/seriesplugin/src/plugin.png differ diff --git a/seriesplugin/src/plugin.py b/seriesplugin/src/plugin.py new file mode 100644 index 000000000..1b3e13d6f --- /dev/null +++ b/seriesplugin/src/plugin.py @@ -0,0 +1,419 @@ +# -*- coding: utf-8 -*- +import os, sys, traceback + +# Localization +from . import _ + +from time import time + +from Components.config import config + +# Plugin +from Components.PluginComponent import plugins +from Plugins.Plugin import PluginDescriptor + +# Plugin internal +from SeriesPluginTimer import SeriesPluginTimer +from SeriesPluginInfoScreen import SeriesPluginInfoScreen +from SeriesPluginRenamer import SeriesPluginRenamer +from SeriesPluginIndependent import startIndependent, runIndependent +from SeriesPluginConfiguration import SeriesPluginConfiguration +from Logger import log + +from spEPGSelection import SPEPGSelectionInit, SPEPGSelectionUndo +from spChannelContextMenu import SPChannelContextMenuInit, SPChannelContextMenuUndo + + +####################################################### +# Constants +NAME = "SeriesPlugin" +VERSION = "5.7" +DESCRIPTION = _("SeriesPlugin") +SHOWINFO = _("Show series info (SP)") +RENAMESERIES = _("Rename serie(s) (SP)") +CHECKTIMERS = _("Check timer list for series (SP)") +SUPPORT = "http://bit.ly/seriespluginihad" +DONATE = "http://bit.ly/seriespluginpaypal" +TERMS = "http://www.serienserver.de" +ABOUT = "\n " + NAME + " " + VERSION + "\n\n" \ + + _(" (C) 2012 by betonme @ IHAD \n\n") \ + + _(" Terms: ") + TERMS + "\n\n" \ + + _(" {lookups:d} successful lookups.\n") \ + + _(" How much time have You saved?\n\n") \ + + _(" Support: ") + SUPPORT + "\n" \ + + _(" Feel free to donate. \n") \ + + _(" PayPal: ") + DONATE + +USER_AGENT = "Enigma2-"+NAME + +try: + from Tools.HardwareInfo import HardwareInfo + DEVICE = HardwareInfo().get_device_name().strip() +except: + DEVICE = '' + +REQUEST_PARAMETER = "?device=" + DEVICE + "&version=SP" + VERSION + +WHERE_EPGMENU = 'WHERE_EPGMENU' +WHERE_CHANNELMENU = 'WHERE_CHANNELMENU' + + +def buildURL(url): + if config.plugins.seriesplugin.proxy_url.value: + return config.plugins.seriesplugin.proxy_url.value + REQUEST_PARAMETER + "&url=" + url + else: + return url + + +####################################################### +# Test +def test(session=None): + # http://dm7080/autotimer + # http://www.unixtime.de/ + try: + #from SeriesPluginBare import bareGetEpisode #future=True, today=False, elapsed=False + #bareGetEpisode("1:0:19:7C:6:85:FFFF0000:0:0:0:", "The Walking Dead", 1448740500, 1448745600, "Description", "/media/hdd/movie", True, False, False) + #bareGetEpisode("1:0:1:2F50:F1:270F:FFFF0000:0:0:0:", "Are You the One?", 1448923500, 1448926500, "Description", "/media/hdd/movie", False, False, True) + #bareGetEpisode("1:0:19:814D:14B:270F:FFFF0000:0:0:0:", "Bones", 1451416200, 1451416200, "Description", "/media/hdd/movie", False, True, False) + #sp = bareGetEpisode("1:0:19:2B66:437:66:FFFF0000:0:0:0:", "Bares für Rares", 1451311500, 1451311500, "Description", "/media/hdd/movie", False, True, False) + #sp = bareGetEpisode("1:0:19:7980:1C3:270F:FFFF0000:0:0:0:", "Offroad Survivors", 1451492100, 1451492100, "Description", "/media/hdd/movie", False, True, False) + #from Tools.Notifications import AddPopup + #from Screens.MessageBox import MessageBox + #AddPopup( sp[0], MessageBox.TYPE_INFO, 0, 'SP_PopUp_ID_Test' ) + + #TEST INFOSCREEN MOVIE + # from enigma import eServiceReference + #service = eServiceReference(eServiceReference.idDVB, 0, "/media/hdd/movie/20151120 0139 - Pro7 HD - The 100.ts") + #service = eServiceReference(eServiceReference.idDVB, 0, "/media/hdd/movie/20151205 1625 - TNT Serie HD (S) - The Last Ship - Staffel 1.ts") + #service = eServiceReference(eServiceReference.idDVB, 0, "/media/hdd/movie/20151204 1825 - VIVA_COMEDY CENTRAL HD - Rules of Engagement.ts") + # movielist_info(session, service) + + #TEST AUTOTIMER + #from SeriesPluginBare import bareGetEpisode + #bareGetEpisode("1:0:1:2F50:F1:270F:FFFF0000:0:0:0:", "Are You the One", 1448751000, 1448754000, "Description", "/media/hdd/movie", False, False, True) + #bareGetEpisode("1:0:19:8150:14B:270F:FFFF0000:0:0:0:", "Dragons Auf zu neuen Ufern TEST_TO_BE_REMOVED", 1449390300, 1449393300, "Description", "/media/hdd/movie", False, False, True) + pass + + except Exception as e: + log.exception(_("SeriesPlugin test exception ") + str(e)) + +####################################################### +# Start +def start(reason, **kwargs): + if config.plugins.seriesplugin.enabled.value: + # Startup + if reason == 0: + + #TEST AUTOTIMER + #test() + #if kwargs.has_key("session"): + # session = kwargs["session"] + # test(session) + #TESTEND + + # Start on demand if it is requested + if config.plugins.seriesplugin.autotimer_independent.value: + startIndependent() + + # Shutdown + elif reason == 1: + from SeriesPlugin import resetInstance + resetInstance() + + +####################################################### +# Plugin configuration +def setup(session, *args, **kwargs): + try: + session.open(SeriesPluginConfiguration) + except Exception as e: + log.exception(_("SeriesPlugin setup exception ") + str(e)) + + +####################################################### +# Event Info +def info(session, service=None, event=None, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + try: + session.open(SeriesPluginInfoScreen, service, event) + except Exception as e: + log.exception(_("SeriesPlugin info exception ") + str(e)) + + +####################################################### +# Extensions menu +def sp_extension(session, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + try: + if session: + session.open(SeriesPluginInfoScreen) + except Exception as e: + log.exception(_("SeriesPlugin extension exception ") + str(e)) + + +####################################################### +# Channel menu +def channel(session, service=None, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + try: + from enigma import eServiceCenter + info = eServiceCenter.getInstance().info(service) + event = info.getEvent(service) + session.open(SeriesPluginInfoScreen, service, event) + except Exception as e: + log.exception(_("SeriesPlugin extension exception ") + str(e)) + + +####################################################### +# Timer +def checkTimers(session, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + runIndependent() + +# Call from timer list - not used yet +def showTimerInfo(session, timer, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + from enigma import eEPGCache + try: + event = timer.eit and epgcache.lookupEventId(timer.service_ref.ref, timer.eit) + session.open(SeriesPluginInfoScreen, timer.service_ref, event) + except Exception as e: + log.exception(_("SeriesPlugin info exception ") + str(e)) + + +####################################################### +# Movielist menu rename +def movielist_rename(session, service, services=None, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + try: + if services: + if not isinstance(services, list): + services = [services] + else: + services = [service] + SeriesPluginRenamer(session, services) + except Exception as e: + log.exception(_("SeriesPlugin renamer exception ") + str(e)) + + +####################################################### +# Movielist menu info +def movielist_info(session, service, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + try: + session.open(SeriesPluginInfoScreen, service) + except Exception as e: + log.exception(_("SeriesPlugin extension exception ") + str(e)) + + +####################################################### +# Timer renaming + +# Synchronous call, blocks until we have the information +def getSeasonEpisode4(service_ref, name, begin, end, description, path, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + from SeriesPluginBare import bareGetEpisode + try: + return bareGetEpisode(service_ref, name, begin, end, description, path, True, False, False) + except Exception as e: + log.exception( "SeriesPlugin getSeasonEpisode4 exception " + str(e)) + return str(e) + +def showResult(*args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + from SeriesPluginBare import bareShowResult + bareShowResult() + + +# Call asynchronous +# Can also be called from a timer list - not used yet +def renameTimer(timer, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + try: + spt = SeriesPluginTimer() + spt.getEpisode(timer) + except Exception as e: + log.exception(_("SeriesPlugin label exception ") + str(e)) + +def renameTimers(timers, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + try: + spt = SeriesPluginTimer() + for timer in timers: + spt.getEpisode(timer) + except Exception as e: + log.exception(_("SeriesPlugin label exception ") + str(e)) + + +####################################################### +# For compatibility reasons +def modifyTimer(timer, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + log.debug("SeriesPlugin modifyTimer is deprecated - Update Your AutoTimer!") + try: + spt = SeriesPluginTimer() + spt.getEpisode(timer) + except Exception as e: + log.exception(_("SeriesPlugin label exception ") + str(e)) + + +# For compatibility reasons +def labelTimer(timer, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + log.debug("SeriesPlugin labelTimer is deprecated - Update Your AutoTimer!") + try: + spt = SeriesPluginTimer() + spt.getEpisode(timer) + except Exception as e: + log.exception(_("SeriesPlugin label exception ") + str(e)) + +# For compatibility reasons +def getSeasonAndEpisode(timer, *args, **kwargs): + result = None + if config.plugins.seriesplugin.enabled.value: + log.debug("SeriesPlugin getSeasonAndEpisode is deprecated - Update Your AutoTimer!") + try: + spt = SeriesPluginTimer() + result = spt.getEpisode(timer, True) + except Exception as e: + log.exception(_("SeriesPlugin label exception ") + str(e)) + return result + +# For compatibility reasons +def getSeasonEpisode(service_ref, name, begin, end, description, path, *args, **kwargs): + if config.plugins.seriesplugin.enabled.value: + log.debug("SeriesPlugin getSeasonEpisode is deprecated - Update Your AutoTimer!") + from SeriesPluginBare import bareGetEpisode + try: + result = bareGetEpisode(service_ref, name, begin, end, description, path) + if result and isinstance(result, dict): + return (result[0],result[1],result[2]) + else: + return str(result) + except Exception as e: + log.exception( "SeriesPlugin getSeasonEpisode4 exception " + str(e)) + return str(e) + + +####################################################### +# Plugin main function +def Plugins(**kwargs): + descriptors = [] + + descriptors.append( PluginDescriptor( + name = NAME + " " + _("Setup"), + description = NAME + " " + _("Setup"), + where = PluginDescriptor.WHERE_PLUGINMENU, + fnc = setup, + needsRestart = False) ) + + if config.plugins.seriesplugin.enabled.value: + + descriptors.append( PluginDescriptor( + where = PluginDescriptor.WHERE_SESSIONSTART, + needsRestart = False, + fnc = start) ) + + if config.plugins.seriesplugin.menu_info.value: + descriptors.append( PluginDescriptor( + name = SHOWINFO, + description = SHOWINFO, + where = PluginDescriptor.WHERE_EVENTINFO, + needsRestart = False, + fnc = info) ) + + if config.plugins.seriesplugin.menu_extensions.value: + descriptors.append(PluginDescriptor( + name = SHOWINFO, + description = SHOWINFO, + where = PluginDescriptor.WHERE_EXTENSIONSMENU, + fnc = sp_extension, + needsRestart = False) ) + + if config.plugins.seriesplugin.check_timer_list.value: + descriptors.append(PluginDescriptor( + name = CHECKTIMERS, + description = CHECKTIMERS, + where = PluginDescriptor.WHERE_EXTENSIONSMENU, + fnc = checkTimers, + needsRestart = False) ) + + if config.plugins.seriesplugin.menu_movie_info.value: + descriptors.append( PluginDescriptor( + name = SHOWINFO, + description = SHOWINFO, + where = PluginDescriptor.WHERE_MOVIELIST, + fnc = movielist_info, + needsRestart = False) ) + + if config.plugins.seriesplugin.menu_movie_rename.value: + descriptors.append( PluginDescriptor( + name = RENAMESERIES, + description = RENAMESERIES, + where = PluginDescriptor.WHERE_MOVIELIST, + fnc = movielist_rename, + needsRestart = False) ) + + if config.plugins.seriesplugin.menu_channel.value: + try: + descriptors.append( PluginDescriptor( + name = SHOWINFO, + description = SHOWINFO, + where = PluginDescriptor.WHERE_CHANNEL_CONTEXT_MENU, + fnc = channel, + needsRestart = False) ) + except: + addSeriesPlugin(WHERE_CHANNELMENU, SHOWINFO) + + if config.plugins.seriesplugin.menu_epg.value: + addSeriesPlugin(WHERE_EPGMENU, SHOWINFO) + + return descriptors + +####################################################### +# Add / Remove menu functions +def addSeriesPlugin(menu, title, fnc=None): + # Add to menu + if( menu == WHERE_EPGMENU ): + SPEPGSelectionInit() + elif( menu == WHERE_CHANNELMENU ): + try: + addSeriesPlugin(PluginDescriptor.WHERE_CHANNEL_CONTEXT_MENU, SHOWINFO, fnc) + except: + SPChannelContextMenuInit() + else: + from Components.PluginComponent import plugins + if plugins: + for p in plugins.getPlugins( where = menu ): + if p.name == title: + # Plugin is already in menu + break + else: + # Plugin not in menu - add it + plugin = PluginDescriptor( + name = title, + description = title, + where = menu, + needsRestart = False, + fnc = fnc) + if menu in plugins.plugins: + plugins.plugins[ menu ].append(plugin) + + +def removeSeriesPlugin(menu, title): + # Remove from menu + if( menu == WHERE_EPGMENU ): + SPEPGSelectionUndo() + elif( menu == WHERE_CHANNELMENU ): + try: + removeSeriesPlugin(PluginDescriptor.WHERE_CHANNEL_CONTEXT_MENU, SHOWINFO) + except: + SPChannelContextMenuUndo() + else: + from Components.PluginComponent import plugins + if plugins: + for p in plugins.getPlugins( where = menu ): + if p.name == title: + plugins.plugins[ menu ].remove(p) + break + diff --git a/seriesplugin/src/spChannelContextMenu.py b/seriesplugin/src/spChannelContextMenu.py new file mode 100644 index 000000000..89de4258d --- /dev/null +++ b/seriesplugin/src/spChannelContextMenu.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +import os, sys, traceback + +# Localization +from . import _ + +from Components.config import config + +# Plugin internal +from Logger import log + + +####################################################### +# Override ChannelContextMenu +ChannelContextMenu__init__ = None +def SPChannelContextMenuInit(): + print "[SeriesPlugin] override ChannelContextMenu.__init__" + global ChannelContextMenu__init__ + if ChannelContextMenu__init__ is None: + from Screens.ChannelSelection import ChannelContextMenu + ChannelContextMenu__init__ = ChannelContextMenu.__init__ + ChannelContextMenu.__init__ = SPChannelContextMenu__init__ + ChannelContextMenu.SPchannelShowSeriesInfo = channelShowSeriesInfo + ChannelContextMenu.SPcloseafterfinish = closeafterfinish + +def SPChannelContextMenuUndo(): + print "[SeriesPlugin] override ChannelContextMenu.__init__" + global ChannelContextMenu__init__ + if ChannelContextMenu__init__: + from Screens.ChannelSelection import ChannelContextMenu + ChannelContextMenu.__init__ = ChannelContextMenu__init__ + ChannelContextMenu__init__ = None + +def SPChannelContextMenu__init__(self, session, csel): + from Components.ChoiceList import ChoiceEntryComponent + from Screens.ChannelSelection import MODE_TV + from Tools.BoundFunction import boundFunction + from enigma import eServiceReference + ChannelContextMenu__init__(self, session, csel) + current = csel.getCurrentSelection() + current_sel_path = current.getPath() + current_sel_flags = current.flags + if csel.mode == MODE_TV and not (current_sel_path or current_sel_flags & (eServiceReference.isDirectory|eServiceReference.isMarker)): + from Plugins.Extensions.SeriesPlugin.plugin import SHOWINFO + self["menu"].list.insert(0, ChoiceEntryComponent(text=(SHOWINFO, boundFunction(self.SPchannelShowSeriesInfo)))) + +def channelShowSeriesInfo(self): + log.debug( "[SeriesPlugin] channelShowSeriesInfo ") + if config.plugins.seriesplugin.enabled.value: + try: + from enigma import eServiceCenter + service = self.csel.servicelist.getCurrent() + info = eServiceCenter.getInstance().info(service) + event = info.getEvent(service) + from Plugins.Extensions.SeriesPlugin.SeriesPluginInfoScreen import SeriesPluginInfoScreen + self.session.openWithCallback(self.SPcloseafterfinish, SeriesPluginInfoScreen, service, event) + except Exception as e: + log.debug(_("SeriesPlugin info exception ") + str(e)) + +def closeafterfinish(self, retval=None): + self.close() + diff --git a/seriesplugin/src/spEPGSelection.py b/seriesplugin/src/spEPGSelection.py new file mode 100644 index 000000000..c212bd54e --- /dev/null +++ b/seriesplugin/src/spEPGSelection.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +import os, sys, traceback + +# Localization +from . import _ + +from Components.config import config + +# Plugin internal +from Logger import log + + +####################################################### +# Override EPGSelection enterDateTime +EPGSelection_enterDateTime = None +#EPGSelection_openOutdatedEPGSelection = None +def SPEPGSelectionInit(): + print "[SeriesPlugin] override EPGSelection" + global EPGSelection_enterDateTime #, EPGSelection_openOutdatedEPGSelection + if EPGSelection_enterDateTime is None: # and EPGSelection_openOutdatedEPGSelection is None: + from Screens.EpgSelection import EPGSelection + EPGSelection_enterDateTime = EPGSelection.enterDateTime + EPGSelection.enterDateTime = enterDateTime + #EPGSelection_openOutdatedEPGSelection = EPGSelection.openOutdatedEPGSelection + #EPGSelection.openOutdatedEPGSelection = openOutdatedEPGSelection + EPGSelection.SPcloseafterfinish = closeafterfinish + +def SPEPGSelectionUndo(): + print "[SeriesPlugin] undo override EPGSelection" + global EPGSelection_enterDateTime #, EPGSelection_openOutdatedEPGSelection + if EPGSelection_enterDateTime: # and EPGSelection_openOutdatedEPGSelection: + from Screens.EpgSelection import EPGSelection + EPGSelection.enterDateTime = EPGSelection_enterDateTime + EPGSelection_enterDateTime = None + #EPGSelection.openOutdatedEPGSelection = EPGSelection_openOutdatedEPGSelection + #EPGSelection_openOutdatedEPGSelection = None + +def enterDateTime(self): + from Screens.EpgSelection import EPG_TYPE_SINGLE,EPG_TYPE_MULTI,EPG_TYPE_SIMILAR + event = self["Event"].event + if self.type == EPG_TYPE_SINGLE: + service = self.currentService + elif self.type == EPG_TYPE_MULTI: + service = self.services + elif self.type == EPG_TYPE_SIMILAR: + service = self.currentService + if service and event: + from Plugins.Extensions.SeriesPlugin.SeriesPluginInfoScreen import SeriesPluginInfoScreen + self.session.openWithCallback(self.SPcloseafterfinish, SeriesPluginInfoScreen, service, event) + return + EPGSelection_enterDateTime(self) + +#def openOutdatedEPGSelection(self, reason=None): +# if reason == 1: +# EPGSelection_enterDateTime(self) + +def closeafterfinish(self, retval=None): + self.close() +