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()
+