diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..de04f93 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,30 @@ +on: [push, pull_request] + +jobs: + kodi-addon-checker: + runs-on: ubuntu-latest + name: Kodi Addon-Checker + steps: + - name: checkout + uses: actions/checkout@v4 + - name: Run Kodi Addon-Checker + id: kodi-addon-checker + uses: xbmc/action-kodi-addon-checker@v1.1 + with: + kodi-version: matrix + addon-id: ${{ github.event.repository.name }} + + autopep8: + runs-on: ubuntu-latest + name: autopep8 + steps: + - name: checkout + uses: actions/checkout@v4 + - name: Run autopep8 + id: autopep8 + uses: peter-evans/autopep8@v2 + with: + args: --aggressive --diff --exit-code --experimental --max-line-length 173 --recursive resources/lib + - name: Fail if autopep8 made changes + if: steps.autopep8.outputs.exit-code == 2 + run: exit 1 diff --git a/README.md b/README.md index e84c4d5..91a9cd9 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,41 @@ -ORF TVthek KODI Addon -======= -ORF TVthek is an addon that gives you access to the ORF TVthek Video Platform. +# ORF ON Addon for Kodi (plugin.video.orftvthek) +ORF ON is an addon that provides access to the ORF ON Video Platform (Austrian Television, formerly ORF TVthek) -Supported platforms -------------------- -Windows, Linux , Android and OSX +[![Kodi version](https://img.shields.io/badge/kodi%20versions-20--21-blue)](https://kodi.tv/) Current Features ---------------- * Livestream -* All Shows -* Schedule Search -* HTTP Stream H264 (Stable) -* Search Function -* Missed Shows -* Blacklist Shows -* JSON(Service API V3) or HTML Scraper -* Restart Livestream - inputstream.adaptive needed +* Shows +* Schedule +* Search +* DRM Streams +* Accessibility Broadcasts +* Simple IPTV Integration + +Todos +---------------- +- [X] Subtitles +- [X] Add Settings +- [X] Add option to show related content +- [X] Add a main menu entry for latest uploads +- [X] Kodi translation still missing +- [X] Accessibility Known Issues ------------ -* you tell me +* A curl bug (http2) on KODI 19 prevents the streaming therefore the Addon is only supported on KODI 20+ (A workaround on the advancedsettings.xml seems to fix the issue, but further testing will be required) +``` + + + true + + +``` Simple IPTV Integration ----------------- @@ -32,52 +43,50 @@ Simple IPTV Integration Playlist Content ``` #EXTINF:-1 tvg-name="ORF 1" tvg-id="orf1" group-title="ORF",ORF 1 -plugin://plugin.video.orftvthek/?channel=orf1&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf1 #EXTINF:-1 tvg-name="ORF 2" tvg-id="orf2" group-title="ORF",ORF 2 -plugin://plugin.video.orftvthek/?channel=orf2&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2 #EXTINF:-1 tvg-name="ORF 3" tvg-id="orf3" group-title="ORF",ORF 3 -plugin://plugin.video.orftvthek/?channel=orf3&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf3 #EXTINF:-1 tvg-name="ORF Sport+" tvg-id="orfs" group-title="ORF",ORF Sport+ -plugin://plugin.video.orftvthek/?channel=orfs&mode=pvr +plugin://plugin.video.orftvthek/pvr/orfs +#EXTINF:-1 tvg-name="ORF Kids" tvg-id="orfkids" group-title="ORF",ORF Kids +plugin://plugin.video.orftvthek/pvr/orfkids #EXTINF:-1 tvg-name="ORF 2 Burgenland" tvg-id="orf2b" group-title="ORF",ORF 2 Burgenland -plugin://plugin.video.orftvthek/?channel=orf2b&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2b #EXTINF:-1 tvg-name="ORF 2 Steiermark" tvg-id="orf2stmk" group-title="ORF",ORF 2 Steiermark -plugin://plugin.video.orftvthek/?channel=orf2stmk&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2stmk #EXTINF:-1 tvg-name="ORF 2 Wien" tvg-id="orf2w" group-title="ORF",ORF 2 Wien -plugin://plugin.video.orftvthek/?channel=orf2w&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2w #EXTINF:-1 tvg-name="ORF 2 Oberösterreich" tvg-id="orf2ooe" group-title="ORF",ORF 2 Oberösterreich -plugin://plugin.video.orftvthek/?channel=orf2ooe&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2ooe #EXTINF:-1 tvg-name="ORF 2 Kärnten" tvg-id="orf2k" group-title="ORF",ORF 2 Kärnten -plugin://plugin.video.orftvthek/?channel=orf2k&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2k #EXTINF:-1 tvg-name="ORF 2 Niederösterreich" tvg-id="orf2n" group-title="ORF",ORF 2 Niederösterreich -plugin://plugin.video.orftvthek/?channel=orf2n&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2n #EXTINF:-1 tvg-name="ORF 2 Salzburg" tvg-id="orf2s" group-title="ORF",ORF 2 Salzburg -plugin://plugin.video.orftvthek/?channel=orf2s&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2s #EXTINF:-1 tvg-name="ORF 2 Vorarlberg" tvg-id="orf2v" group-title="ORF",ORF 2 Vorarlberg -plugin://plugin.video.orftvthek/?channel=orf2v&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2v #EXTINF:-1 tvg-name="ORF 2 Tirol" tvg-id="orf2t" group-title="ORF",ORF 2 Tirol -plugin://plugin.video.orftvthek/?channel=orf2t&mode=pvr +plugin://plugin.video.orftvthek/pvr/orf2t ``` Legal ----- -This addon provides access to videos on the ORF TVthek Website but is not endorsed, certified or otherwise approved in any way by ORF. - -Icons ------ -https://uxwing.com +This addon provides access to videos on the ORF ON Website but is not endorsed, certified or otherwise approved in any way by ORF. diff --git a/addon.xml b/addon.xml index 22bf69a..4b343f0 100644 --- a/addon.xml +++ b/addon.xml @@ -1,34 +1,34 @@ - + - - - - + + - + video + all de en - ORF TVthek - ORF TVthek - ORF TVthek - Ermöglicht Ihnen den Zugriff auf die ORF TVthek Video Platform - ORF TVthek - This plugin provides access to the Austrian "ORF TVthek" + ORF ON + ORF ON + ORF ON - Dieses Plugin ermöglicht den Zugriff auf den österreichischen Streamingdienst ORF ON + ORF ON - This plugin provides access to the Austrian ORF ON streaming service GPL-2.0-only - https://forum.kodi.tv/showthread.php?tid=159835 + https://forum.kodi.tv/ sofaking@gettingmoney.at - https://tvthek.orf.at + https://on.orf.at https://github.com/s0faking/plugin.video.orftvthek resources/icon.png resources/fanart.jpg - v0.12.12 (23/05/2024) - [info] ORF TVthek is down and replaced by ORF On - [fix] ServiceAPI is forced on this version + v1.0.2 + - new Livestream (timeshift) + - added setting to use old livestream format + - LF conversion diff --git a/changelog.txt b/changelog.txt index ca278d2..8877648 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,23 @@ +v1.0.2 (2024-06-08) +- new Livestream (timeshift) +- added setting to use old livestream format +- LF conversion + +v1.0.1 beta (2024-06-05) +- fix routing requirement +- show segment option added +- cache reload fix +- related video option +- fix UA stream errors (#6) +- orf tvthek has been disabled + +v1.0.0 beta (2024-01-06) +- inital ORF ON addon +- beta version +- most stuff is working but everything needs to be tested a little more +- orf on is still in beta so stuff might change until the final release in april 2024 +- translation stuff is still missing + * 0.12.12 - ORF TVthek is down and replaced by ORF On - ServiceAPI is forced on this version @@ -250,4 +270,4 @@ - Streaming Video from http://tvthek.orf.at (mms) - Experimental High Quality MP4 Streaming from http://tvthek.orf.at (rtmp) - Livestream -- Missed Shows +- Missed Shows \ No newline at end of file diff --git a/resources/banner.jpg b/resources/banner.jpg new file mode 100644 index 0000000..e123f0a Binary files /dev/null and b/resources/banner.jpg differ diff --git a/resources/fanart.jpg b/resources/fanart.jpg index d0c00d9..b4f8dcc 100644 Binary files a/resources/fanart.jpg and b/resources/fanart.jpg differ diff --git a/resources/icon.png b/resources/icon.png index 02e2da3..3d519c2 100644 Binary files a/resources/icon.png and b/resources/icon.png differ diff --git a/resources/landscape.jpg b/resources/landscape.jpg new file mode 100644 index 0000000..8c9ff23 Binary files /dev/null and b/resources/landscape.jpg differ diff --git a/resources/language/resource.language.de_de/strings.po b/resources/language/resource.language.de_de/strings.po index 60dd793..1e86ad7 100644 --- a/resources/language/resource.language.de_de/strings.po +++ b/resources/language/resource.language.de_de/strings.po @@ -1,266 +1,193 @@ # XBMC Media Center language file -# Addon Name: ORF TVthek +# Addon Name: ORF ON # Addon id: plugin.video.orftvthek -# Addon version: 0.9.0 +# Addon version: 1.0.2 # Addon Provider: sofaking msgid "" msgstr "" "Project-Id-Version: XBMC-Addons\n" "Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2017-02-05 22:17+0000\n" -"PO-Revision-Date: 2019-11-09 14:05+0000\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE\n" +"POT-Creation-Date: 2024-01-06 00:00+0000\n" +"PO-Revision-Date: 2024-01-06 00:00+0000\n" +"Last-Translator: sofaking \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -msgctxt "Addon Summary" -msgid "ORF TVthek" -msgstr "ORF TVthek" - -msgctxt "Addon Description" -msgid "ORF TVthek - This plugin provides access to the Austrian \"ORF TVthek\"" -msgstr "ORF TVthek - Ermöglicht Ihnen den Zugriff auf die ORF TVthek Video Platform" - -msgctxt "#30000" -msgid "Recently Added Shows" -msgstr "Neuste Sendungen" - -msgctxt "#30001" -msgid "Frontpage" -msgstr "Startseite" - -msgctxt "#30002" -msgid "Shows" -msgstr "Sendungen" - -msgctxt "#30003" -msgid "Topics" -msgstr "Themen" - -msgctxt "#30004" -msgid "Livestream" -msgstr "Live" - -msgctxt "#30005" -msgid "ORF Recommendations" -msgstr "ORF Tipps" - -msgctxt "#30006" -msgid "Most Viewed" -msgstr "Meist gesehen" - -msgctxt "#30007" -msgid "Search" -msgstr "Suchen" - -msgctxt "#30009" -msgid "Broadcasted: " -msgstr "Sendung vom" - -msgctxt "#30011" -msgid "Runtime" -msgstr "Laufzeit" - -msgctxt "#30014" -msgid "No Results" -msgstr "Keine Ergebnisse" - -msgctxt "#30015" -msgid "Play all" -msgstr "Alle Beiträge abspielen" - -msgctxt "#30018" -msgid "Missed a Show?" -msgstr "Sendung verpasst?" - -msgctxt "#30019" -msgid "Streaming" -msgstr "Livestream läuft bereits" - -msgctxt "#30020" -msgid "Stream Offline" -msgstr "Livestream läuft noch nicht" - -msgctxt "#30022" -msgid "Video Resolution" -msgstr "Videoqualität" +msgctxt "#30101" +msgid "General" +msgstr "Allgemein" -msgctxt "#30023" -msgid "Low" -msgstr "Niedrig" +msgctxt "#30102" +msgid "Use subtitles" +msgstr "Untertitel anzeigen" -msgctxt "#30024" -msgid "Medium" -msgstr "Mittel" +msgctxt "#30103" +msgid "Hide sign language content" +msgstr "Gebärdensprache Inhalte ausblenden" -msgctxt "#30025" -msgid "High" -msgstr "Hoch" +msgctxt "#30104" +msgid "Hide Audio description content" +msgstr "Audiodeskription Inhalte ausblenden" -msgctxt "#30026" -msgid "Use ServiceAPI (mad props to @Rechi)" -msgstr "ServiceAPI verwenden (mad props to @Rechi)" +msgctxt "#30105" +msgid "Items per page" +msgstr "Inhalte pro Seite" -msgctxt "#30027" -msgid "Trailers" -msgstr "TV-Vorschau" +msgctxt "#30106" +msgid "Cache" +msgstr "Cache" -msgctxt "#30028" -msgid "Show Autoplay Prompt" -msgstr "Beim Öffnen Aktion nachfragen" +msgctxt "#30107" +msgid "Reload channel/settings Cache" +msgstr "Sender/Konfigurations Cache neu laden" -msgctxt "#30029" -msgid "Use Subtitles" -msgstr "Untertitel verwenden" +msgctxt "#30108" +msgid "Cache validity (days)" +msgstr "Cache Gültigkeit (Tage)" -msgctxt "#30030" -msgid "The broadcast hasn't started yet" -msgstr "Livestream noch nicht gestartet" +msgctxt "#30109" +msgid "Clear search history" +msgstr "Suchverlauf löschen" -msgctxt "#30031" -msgid "The broadcast starts at" -msgstr "Der Livestream startet erst um" +msgctxt "#30110" +msgid "Frontpage" +msgstr "Startseite" -msgctxt "#30032" -msgid "Do you want the stream to start automatically?" -msgstr "Soll der Livesteam automatisch starten?" +msgctxt "#30111" +msgid "Schedule" +msgstr "Sendung verpasst" -msgctxt "#30033" -msgid "Waiting for the broadcast to start" -msgstr "Warte auf Sendungsbeginn" +msgctxt "#30112" +msgid "Shows" +msgstr "Sendungen" -msgctxt "#30034" -msgid "Time till broadcast:" -msgstr "Zeit bis zum Start:" +msgctxt "#30113" +msgid "Livestream" +msgstr "Livestream" -msgctxt "#30035" -msgid "Start the stream?" -msgstr "Den Livestream Starten?" +msgctxt "#30114" +msgid "Search" +msgstr "Suche" -msgctxt "#30037" -msgid "Blacklist" -msgstr "Ausgeblendete Sendungen" +msgctxt "#30115" +msgid "Highlights" +msgstr "Highlights" -msgctxt "#30038" -msgid "# Blacklist" -msgstr "# " +msgctxt "#30116" +msgid "Categories" +msgstr "Kategorien" -msgctxt "#30040" -msgid "# Remove %s from the Blacklist #" -msgstr "# %s von der Liste entfernen #" +msgctxt "#30124" +msgid "All episode results" +msgstr "Alle Episoden Ergebnisse" -msgctxt "#30042" -msgid " #" -msgstr "ausblenden #" +msgctxt "#30125" +msgid "All chapter results" +msgstr "Alle Kapitel Ergebnisse" -msgctxt "#30043" -msgid "Use Blacklist Feature" -msgstr "\"Sendungen ausblenden\" aktivieren" +msgctxt "#30126" +msgid "All history results" +msgstr "Alle ORF History Ergebnisse" -msgctxt "#30044" -msgid "HD" -msgstr "Sehr Hoch(HD)" +msgctxt "#30127" +msgid "Next page" +msgstr "Nächste Seite" -msgctxt "#30045" -msgid "The ServiceAPI is currently offline" -msgstr "Die ServiceAPI ist zurzeit Offline" +msgctxt "#30128" +msgid "Geo lock active" +msgstr "Geo Lock aktiv" -msgctxt "#30046" -msgid "Switch back to HTML Parsing in the Addon Settings" -msgstr "Deaktivieren Sie die Option in den Einstellungen" +msgctxt "#30129" +msgid "Some content may not be available in your country" +msgstr "Einige Inhalte sind in Ihrem Land unter Umständen nicht verfügbar" -msgctxt "#30047" -msgid "Play Video" -msgstr "Video abspielen" +msgctxt "#30130" +msgid "Select a date" +msgstr "Datum auswählen" -msgctxt "#30048" -msgid "Do you want the playlist to start immediately?" -msgstr "Soll die Playlist sofort gestartet werden?" +msgctxt "#30131" +msgid "Enter search" +msgstr "Suchbegriff eingeben" -msgctxt "#30049" -msgid "Archive" -msgstr "Archiv" +msgctxt "#30132" +msgid "Clearing search history" +msgstr "Suchverlauf löschen" -msgctxt "#30050" -msgid "An Error occured while loading the selected playlist" -msgstr "Die Playlist konnte nicht abgespielt werden." +msgctxt "#30133" +msgid "Clearing" +msgstr "Wird gelöscht" -msgctxt "#30051" -msgid "Error" -msgstr "Fehler" +msgctxt "#30134" +msgid "Done" +msgstr "Fertig" -msgctxt "#30052" -msgid "This Video is offline" -msgstr "Dieses Video ist offline" +msgctxt "#30136" +msgid "Reloading cache" +msgstr "Lade Cache neu" -msgctxt "#30053" -msgid "Adaptive" -msgstr "Adaptiv" +msgctxt "#30137" +msgid "Loading channels" +msgstr "Sender werden geladen" -msgctxt "#30054" -msgid "Stream Method" -msgstr "Stream Methode" +msgctxt "#30138" +msgid "Loading settings" +msgstr "Einstellungen werden geladen" -msgctxt "#30055" -msgid "HLS" -msgstr "" - -msgctxt "#30056" -msgid "Enable Progressive Streaming Method" -msgstr "Progressive Streaming aktivieren" +msgctxt "#30139" +msgid "Restart" +msgstr "Restart" -msgctxt "#30057" -msgid "In Focus" -msgstr "Im Fokus" +msgctxt "#30140" +msgid "All episodes" +msgstr "Alle Folgen" -msgctxt "#30058" -msgid "Show 'Play Chapters'" -msgstr "'Kapitel abspielen' anzeigen" +msgctxt "#30141" +msgid "Episodes" +msgstr "Folgen" -msgctxt "#30059" -msgid "Play Full Video" -msgstr "Ganzes Video abspielen" +msgctxt "#30142" +msgid "Channel" +msgstr "Sender" -msgctxt "#30060" -msgid "Play Chapters" -msgstr "Kapitel abspielen" +msgctxt "#30143" +msgid "Starts in" +msgstr "Startet in" -msgctxt "#30061" -msgid "Show Full Livestream Schedule" -msgstr "Livestream Programm anzeigen" +msgctxt "#30144" +msgid "Recently added" +msgstr "Neuste Sendungen" -msgctxt "#30062" -msgid "General" -msgstr "Allgemein" +msgctxt "#30145" +msgid "Broadcasts using sign language" +msgstr "Sendungen mit Gebärdensprache" -msgctxt "#30063" -msgid "Restart" -msgstr "Restart" +msgctxt "#30146" +msgid "Broadcasts with audio description" +msgstr "Sendungen mit Audiodeskription" -msgctxt "#30064" -msgid "older than" -msgstr "älter als" +msgctxt "#30147" +msgid "Accessibility" +msgstr "Barrierefrei" -msgctxt "#30065" -msgid "Aktivieren Sie Inputstream Adaptive um alle Livestreams zu sehen" -msgstr "" +msgctxt "#30148" +msgid "Broadcasts with subtitles" +msgstr "Sendungen mit Untertitel" -msgctxt "#30066" -msgid "Inputstream Adaptive ist nicht aktiviert. DRM Inhalte können nicht abgespielt werden." -msgstr "" +msgctxt "#30149" +msgid "Use segements" +msgstr "Kapitel verwenden" -msgctxt "#30067" -msgid "Inputstream Helper ist nicht aktiviert. DRM Inhalte können nicht abgespielt werden." -msgstr " +msgctxt "#30150" +msgid "Related content" +msgstr "Ähnliche Inhalte" -msgctxt "#30068" -msgid "ORF TVthek offline" -msgstr " +msgctxt "#30151" +msgid "Show segements" +msgstr "Kapitel anzeigen" -msgctxt "#30069" -msgid "Die ORF TVthek wurde eingestellt und wird von ORF ON ersetzt. Das Addon funktioniert ab sofort nur mehr mit der ServiceAPI." -msgstr " \ No newline at end of file +msgctxt "#30152" +msgid "Use timeshift" +msgstr "Timeshift verwenden" \ No newline at end of file diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 327f1f6..b291dc8 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1,266 +1,193 @@ # XBMC Media Center language file -# Addon Name: ORF TVthek +# Addon Name: ORF ON # Addon id: plugin.video.orftvthek -# Addon version: 0.9.0 +# Addon version: 1.0.2 # Addon Provider: sofaking msgid "" msgstr "" "Project-Id-Version: XBMC-Addons\n" "Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" -"POT-Creation-Date: 2017-02-05 22:17+0000\n" -"PO-Revision-Date: 2019-11-09 14:00+0000\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE\n" +"POT-Creation-Date: 2024-01-06 00:00+0000\n" +"PO-Revision-Date: 2024-01-06 00:00+0000\n" +"Last-Translator: sofaking \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: en\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -msgctxt "Addon Summary" -msgid "ORF TVthek" -msgstr "" - -msgctxt "Addon Description" -msgid "ORF TVthek - This plugin provides access to the Austrian \"ORF TVthek\"" -msgstr "" - -msgctxt "#30000" -msgid "Recently Added Shows" -msgstr "" - -msgctxt "#30001" -msgid "Frontpage" -msgstr "" - -msgctxt "#30002" -msgid "Shows" -msgstr "" - -msgctxt "#30003" -msgid "Topics" -msgstr "" - -msgctxt "#30004" -msgid "Livestream" -msgstr "" - -msgctxt "#30005" -msgid "ORF Recommendations" -msgstr "" - -msgctxt "#30006" -msgid "Most Viewed" -msgstr "" - -msgctxt "#30007" -msgid "Search" -msgstr "" - -msgctxt "#30009" -msgid "Broadcasted: " -msgstr "" - -msgctxt "#30011" -msgid "Runtime" -msgstr "" - -msgctxt "#30014" -msgid "No Results" -msgstr "" - -msgctxt "#30015" -msgid "Play all" -msgstr "" - -msgctxt "#30018" -msgid "Missed a Show?" -msgstr "" - -msgctxt "#30019" -msgid "Streaming" -msgstr "" - -msgctxt "#30020" -msgid "Stream Offline" -msgstr "" - -msgctxt "#30022" -msgid "Video Resolution" +msgctxt "#30101" +msgid "General" msgstr "" -msgctxt "#30023" -msgid "Low" +msgctxt "#30102" +msgid "Use subtitles" msgstr "" -msgctxt "#30024" -msgid "Medium" +msgctxt "#30103" +msgid "Hide sign language content" msgstr "" -msgctxt "#30025" -msgid "High" +msgctxt "#30104" +msgid "Hide Audio description content" msgstr "" -msgctxt "#30026" -msgid "Use ServiceAPI (mad props to @Rechi)" +msgctxt "#30105" +msgid "Items per page" msgstr "" -msgctxt "#30027" -msgid "Trailers" +msgctxt "#30106" +msgid "Cache" msgstr "" -msgctxt "#30028" -msgid "Show Autoplay Prompt" +msgctxt "#30107" +msgid "Reload channel/settings Cache" msgstr "" -msgctxt "#30029" -msgid "Use Subtitles" +msgctxt "#30108" +msgid "Cache validity (days)" msgstr "" -msgctxt "#30030" -msgid "The broadcast hasn't started yet" +msgctxt "#30109" +msgid "Clear search history" msgstr "" -msgctxt "#30031" -msgid "The broadcast starts at" +msgctxt "#30110" +msgid "Frontpage" msgstr "" -msgctxt "#30032" -msgid "Do you want the stream to start automatically?" +msgctxt "#30111" +msgid "Schedule" msgstr "" -msgctxt "#30033" -msgid "Waiting for the broadcast to start" +msgctxt "#30112" +msgid "Shows" msgstr "" -msgctxt "#30034" -msgid "Time till broadcast:" +msgctxt "#30113" +msgid "Livestream" msgstr "" -msgctxt "#30035" -msgid "Start the stream?" +msgctxt "#30114" +msgid "Search" msgstr "" -msgctxt "#30037" -msgid "Blacklist" +msgctxt "#30115" +msgid "Highlights" msgstr "" -msgctxt "#30038" -msgid "# Blacklist" +msgctxt "#30116" +msgid "Categories" msgstr "" -msgctxt "#30040" -msgid "# Remove %s from the Blacklist #" +msgctxt "#30124" +msgid "All episode results" msgstr "" -msgctxt "#30042" -msgid " #" +msgctxt "#30125" +msgid "All chapter results" msgstr "" -msgctxt "#30043" -msgid "Use Blacklist Feature" +msgctxt "#30126" +msgid "All history results" msgstr "" -msgctxt "#30044" -msgid "HD" +msgctxt "#30127" +msgid "Next page" msgstr "" -msgctxt "#30045" -msgid "The ServiceAPI is currently offline" +msgctxt "#30128" +msgid "Geo lock active" msgstr "" -msgctxt "#30046" -msgid "Switch back to HTML Parsing in the Addon Settings" +msgctxt "#30129" +msgid "Some content may not be available in your country" msgstr "" -msgctxt "#30047" -msgid "Play Video" +msgctxt "#30130" +msgid "Select a date" msgstr "" -msgctxt "#30048" -msgid "Do you want the playlist to start immediately?" +msgctxt "#30131" +msgid "Enter search" msgstr "" -msgctxt "#30049" -msgid "Archive" +msgctxt "#30132" +msgid "Clearing search history" msgstr "" -msgctxt "#30050" -msgid "An Error occured while loading the selected playlist" +msgctxt "#30133" +msgid "Clearing" msgstr "" -msgctxt "#30051" -msgid "Error" +msgctxt "#30134" +msgid "Done" msgstr "" -msgctxt "#30052" -msgid "This Video is offline" +msgctxt "#30136" +msgid "Reloading cache" msgstr "" -msgctxt "#30053" -msgid "Adaptive" +msgctxt "#30137" +msgid "Loading channels" msgstr "" -msgctxt "#30054" -msgid "Stream Method" +msgctxt "#30138" +msgid "Loading settings" msgstr "" -msgctxt "#30055" -msgid "HLS" +msgctxt "#30139" +msgid "Restart" msgstr "" -msgctxt "#30056" -msgid "Enable Progressive Streaming Method" +msgctxt "#30140" +msgid "All episodes" msgstr "" -msgctxt "#30057" -msgid "In Focus" +msgctxt "#30141" +msgid "Episodes" msgstr "" -msgctxt "#30058" -msgid "Show 'Play Chapters'" +msgctxt "#30142" +msgid "Channel" msgstr "" -msgctxt "#30059" -msgid "Play Full Video" +msgctxt "#30143" +msgid "Starts in" msgstr "" -msgctxt "#30060" -msgid "Play Chapters" +msgctxt "#30144" +msgid "Recently added" msgstr "" -msgctxt "#30061" -msgid "Show Full Livestream Schedule" +msgctxt "#30145" +msgid "Broadcasts using sign language" msgstr "" -msgctxt "#30062" -msgid "General" +msgctxt "#30146" +msgid "Broadcasts with audio description" msgstr "" -msgctxt "#30063" -msgid "Restart" +msgctxt "#30147" +msgid "Accessibility" msgstr "" -msgctxt "#30064" -msgid "older than" +msgctxt "#30148" +msgid "Broadcasts with subtitles" msgstr "" -msgctxt "#30065" -msgid "Install Inputstream Adaptive to see more Livestreams" +msgctxt "#30149" +msgid "Use segements" msgstr "" -msgctxt "#30066" -msgid "Inputstream Adaptive not installed. Cant play DRM content." +msgctxt "#30150" +msgid "Related content" msgstr "" -msgctxt "#30067" -msgid "Inputstream Helper not installed. Cant play DRM content." +msgctxt "#30151" +msgid "Show segements" msgstr "" -msgctxt "#30068" -msgid "ORF TVthek offline" -msgstr " - -msgctxt "#30069" -msgid "The ORF TVthek has been discontinued and is being replaced by ORF ON. From now on the addon only works with the ServiceAPI." -msgstr " \ No newline at end of file +msgctxt "#30152" +msgid "Use timeshift" +msgstr "" \ No newline at end of file diff --git a/resources/lib/Addon.py b/resources/lib/Addon.py index 07692a0..7edc1c2 100644 --- a/resources/lib/Addon.py +++ b/resources/lib/Addon.py @@ -1,324 +1,283 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -import socket -import traceback -import xbmcplugin - -from resources.lib.ServiceApi import * -from resources.lib.HtmlScraper import * - -socket.setdefaulttimeout(30) - -plugin = "ORF-TVthek-" + xbmcaddon.Addon().getAddonInfo('version') - -# initial -settings = xbmcaddon.Addon() -pluginhandle = int(sys.argv[1]) -basepath = settings.getAddonInfo('path') -translation = settings.getLocalizedString - -# hardcoded -videoProtocol = "http" -videoQuality = "QXB" -videoDelivery = "HLS" - -input_stream_protocol = 'mpd' -input_stream_drm_version = 'com.widevine.alpha' -input_stream_mime = 'application/dash+xml' -input_stream_lic_content_type = 'application/octet-stream' - -# media resources -resource_path = os.path.join(basepath, "resources") -media_path = os.path.join(resource_path, "media") -defaultbanner = os.path.join(media_path, "default_banner_v2.jpg") -news_banner = os.path.join(media_path, "news_banner_v2.jpg") -recently_added_banner = os.path.join(media_path, "recently_added_banner_v2.jpg") -shows_banner = os.path.join(media_path, "shows_banner_v2.jpg") -topics_banner = os.path.join(media_path, "topics_banner_v2.jpg") -live_banner = os.path.join(media_path, "live_banner_v2.jpg") -tips_banner = os.path.join(media_path, "tips_banner_v2.jpg") -most_popular_banner = os.path.join(media_path, "most_popular_banner_v2.jpg") -schedule_banner = os.path.join(media_path, "schedule_banner_v2.jpg") -archive_banner = os.path.join(media_path, "archive_banner_v2.jpg") -search_banner = os.path.join(media_path, "search_banner_v2.jpg") -trailer_banner = os.path.join(media_path, "trailer_banner_v2.jpg") -blacklist_banner = os.path.join(media_path, "blacklist_banner_v2.jpg") -focus_banner = os.path.join(media_path, "focus_banner_v2.jpg") -defaultbackdrop = os.path.join(media_path, "fanart_v2.jpg") - -# load settings -useServiceAPI = True -autoPlayPrompt = Settings.autoPlayPrompt() -usePlayAllPlaylist = Settings.playAllPlaylist() -showWarning = Settings.showWarning() - -# init scrapers -if useServiceAPI: - debugLog("Service API activated") - scraper = serviceAPI(xbmc, settings, pluginhandle, videoQuality, videoProtocol, videoDelivery, defaultbanner, defaultbackdrop, usePlayAllPlaylist) -else: - debugLog("HTML Scraper activated") - scraper = htmlScraper(xbmc, settings, pluginhandle, videoQuality, videoProtocol, videoDelivery, defaultbanner, defaultbackdrop, usePlayAllPlaylist) - - -def getMainMenu(): - if showWarning: - d = xbmcgui.Dialog() - d.ok((translation(30068)).encode("utf-8"), (translation(30069)).encode("utf-8")) - xbmcaddon.Addon('plugin.video.orftvthek').setSettingBool('showWarning', 0) - debugLog("Building Main Menu") - addDirectory((translation(30001)).encode("utf-8"), news_banner, defaultbackdrop, "", "", "getAktuelles", pluginhandle) - addDirectory((translation(30000)).encode("utf-8"), recently_added_banner, defaultbackdrop, "", "", "getNewShows", pluginhandle) - addDirectory((translation(30002)).encode("utf-8"), shows_banner, defaultbackdrop, "", "", "getSendungen", pluginhandle) - if useServiceAPI: - addDirectory((translation(30003)).encode("utf-8"), topics_banner, defaultbackdrop, "", "", "getThemen", pluginhandle) - addDirectory((translation(30004)).encode("utf-8"), live_banner, defaultbackdrop, "", "", "getLive", pluginhandle) - if not useServiceAPI: - addDirectory((translation(30057)).encode("utf-8"), focus_banner, defaultbackdrop, "", "", "getFocus", pluginhandle) - addDirectory((translation(30006)).encode("utf-8"), most_popular_banner, defaultbackdrop, "", "", "getMostViewed", pluginhandle) - addDirectory((translation(30018)).encode("utf-8"), schedule_banner, defaultbackdrop, "", "", "getSchedule", pluginhandle) - if not useServiceAPI: - addDirectory((translation(30049)).encode("utf-8"), archive_banner, defaultbackdrop, "", "", "getArchiv", pluginhandle) - addDirectory((translation(30027)).encode("utf-8"), trailer_banner, defaultbackdrop, "", "", "openTrailers", pluginhandle) - if not useServiceAPI: - addDirectory((translation(30007)).encode("utf-8"), search_banner, defaultbackdrop, "", "", "getSearchHistory", pluginhandle) - if Settings.blacklist() and not useServiceAPI: - addDirectory((translation(30037)).encode("utf-8"), blacklist_banner, defaultbackdrop, "", "", "openBlacklist", pluginhandle) - listCallback(False, pluginhandle) - - -def listCallback(sort, pluginhandle): - xbmcplugin.setContent(pluginhandle, 'episodes') - if sort: - xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_VIDEO_TITLE) - xbmcplugin.endOfDirectory(pluginhandle) - - -def startPlaylist(player, playlist): - if playlist is not None: - player.play(playlist) +import sys + +from Kodi import * +import routing + +settings_file = 'settings.json' +channel_map_file = 'channels.json' +search_history_file = 'search_history' + +route_plugin = routing.Plugin() +kodi_worker = Kodi(route_plugin) +if not sys.argv[0].startswith('plugin://' + kodi_worker.addon_id + '/dialog'): + channel_map, channel_map_cached = kodi_worker.get_cached_file(channel_map_file) + settings, settings_cached = kodi_worker.get_cached_file(settings_file) + api = OrfOn(channel_map=channel_map, settings=settings, useragent=kodi_worker.useragent, kodi_worker=kodi_worker) + api.set_pager_limit(kodi_worker.pager_limit) + api.set_segments_behaviour(kodi_worker.use_segments) + + kodi_worker.set_geo_lock(api.is_geo_locked()) + channel_map = api.get_channel_map() + settings = api.get_settings() + + # Only overwrite if cache was invalidated + if not channel_map_cached: + kodi_worker.save_json(channel_map, channel_map_file) + + if not settings_cached: + kodi_worker.save_json(settings, settings_file) + + +@route_plugin.route('/') +def get_main_menu(): + kodi_worker.log("Loading Main Menu", 'route') + if kodi_worker.is_geo_locked(): + kodi_worker.render( + Directory( + kodi_worker.get_translation(30128, 'Geo lock active', ' [COLOR red]*** %s ***[/COLOR]'), + kodi_worker.get_translation(30129, 'Some content may not be available in your country'), + '/', translator=kodi_worker)) + index_directories = api.get_main_menu() + for index_directory in index_directories: + kodi_worker.render(index_directory) + if not kodi_worker.hide_accessibility_menu(): + kodi_worker.render(Directory(kodi_worker.get_translation(30147, 'Accessibility'), '', '/accessibility', '', 'accessibility', translator=kodi_worker)) + kodi_worker.list_callback() + + +@route_plugin.route('/page/start') +def get_frontpage(): + kodi_worker.log("Loading Frontpage Teasers", 'route') + teasers = api.get_frontpage() + for teaser in teasers: + kodi_worker.render(teaser) + kodi_worker.list_callback() + + +@route_plugin.route('/accessibility') +def get_accessibility_menu(): + if not kodi_worker.hide_sign_language(): + kodi_worker.render(api.get_sign_language_menu()) + if not kodi_worker.hide_audio_description(): + kodi_worker.render(api.get_audio_description_menu()) + if kodi_worker.use_subtitles: + kodi_worker.render(api.get_subtitles_menu()) + kodi_worker.list_callback() + + +@route_plugin.route('/livestreams') +def get_livestreams(): + kodi_worker.log("Loading Livestream Overview", 'route') + streams = api.get_live_schedule() + for stream in streams: + kodi_worker.render(stream) + kodi_worker.list_callback() + + +@route_plugin.route('/restart/') +def get_live_restart(livestreamid): + kodi_worker.log("Playing Livestream Restart %s" % livestreamid, 'route') + livestream_item = api.get_livestream(livestreamid) + livestream_item = api.get_restart_stream(livestream_item) + kodi_worker.restart(livestream_item) + + +@route_plugin.route('/profile/') +def get_profile(profileid): + kodi_worker.log("Loading Profile %s" % profileid, 'route') + request_url = '/profile/%s' % profileid + directories = api.get_url(request_url) + if len(directories) > 1: + for directory in directories: + kodi_worker.render(directory) + kodi_worker.list_callback() else: - d = xbmcgui.Dialog() - d.ok((translation(30051)).encode("utf-8"), (translation(30050)).encode("utf-8")) + videos = api.load_stream_data('/profile/%s/episodes' % profileid) + kodi_worker.play(videos) -def run(): - # video playback - tvthekplayer = xbmc.Player() - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - - # parameters - params = parameters_string_to_dict(sys.argv[2]) - mode = params.get('mode') - link = params.get('link') - if mode == 'openSeries': - playlist.clear() - playlist = scraper.getLinks(link, params.get('banner'), playlist) - if autoPlayPrompt and playlist is not None: - listCallback(False, pluginhandle) - ok = xbmcgui.Dialog().yesno((translation(30047)).encode("utf-8"), (translation(30048)).encode("utf-8")) - listCallback(False, pluginhandle) - if ok: - debugLog("Starting Playlist for %s" % unqoute_url(link)) - tvthekplayer.play(playlist) - else: - debugLog("Running Listcallback from no autoplay openseries") - listCallback(False, pluginhandle) - elif mode == 'unblacklistShow': - heading = translation(30040).encode('UTF-8') % unqoute_url(link).replace('+', ' ').strip() - if isBlacklisted(link) and xbmcgui.Dialog().yesno(heading, heading): - unblacklistItem(link) - xbmc.executebuiltin('Container.Refresh') - elif mode == 'blacklistShow': - blacklistItem(link) - xbmc.executebuiltin('Container.Refresh') - if mode == 'openBlacklist': - printBlacklist(defaultbanner, defaultbackdrop, translation, pluginhandle) - xbmcplugin.endOfDirectory(pluginhandle) - elif mode == 'getSendungen': - scraper.getCategories() - listCallback(True, pluginhandle) - elif mode == 'getAktuelles': - scraper.getHighlights() - listCallback(False, pluginhandle) - elif mode == 'getLive': - scraper.getLiveStreams() - listCallback(False, pluginhandle) - elif mode == 'getTipps': - scraper.getTips() - listCallback(False, pluginhandle) - elif mode == 'getFocus': - scraper.getFocus() - listCallback(False, pluginhandle) - elif mode == 'getNewShows': - scraper.getNewest() - listCallback(False, pluginhandle) - elif mode == 'getMostViewed': - scraper.getMostViewed() - listCallback(False, pluginhandle) - elif mode == 'getThemen': - scraper.getThemen() - listCallback(True, pluginhandle) - elif mode == 'getSendungenDetail': - scraper.getCategoriesDetail(link, params.get('banner')) - listCallback(False, pluginhandle) - elif mode == 'getThemenDetail': - scraper.getArchiveDetail(link) - listCallback(False, pluginhandle) - elif mode == 'getArchiveDetail': - scraper.getArchiveDetail(link) - listCallback(False, pluginhandle) - elif mode == 'getSchedule': - scraper.getSchedule() - listCallback(False, pluginhandle) - elif mode == 'getArchiv': - scraper.getArchiv() - listCallback(False, pluginhandle) - elif mode == 'getScheduleDetail': - scraper.openArchiv(link) - listCallback(True, pluginhandle) - elif mode == 'openTrailers': - scraper.getTrailers() - listCallback(False, pluginhandle) - elif mode == 'getSearchHistory': - scraper.getSearchHistory() - listCallback(False, pluginhandle) - elif mode == 'getSearchResults': - if link is not None: - scraper.getSearchResults(unqoute_url(link)) - else: - scraper.getSearchResults("") - listCallback(False, pluginhandle) - elif mode == 'openDate': - scraper.getDate(link, params.get('from')) - listCallback(False, pluginhandle) - elif mode == 'openProgram': - scraper.getProgram(link, playlist) - listCallback(False, pluginhandle) - elif mode == 'openTopic': - scraper.getTopic(link) - listCallback(False, pluginhandle) - elif mode == 'openEpisode': - scraper.getEpisode(link, playlist) - listCallback(False, pluginhandle) - elif mode == 'liveStreamRestart': - try: - import inputstreamhelper - is_helper = inputstreamhelper.Helper(input_stream_protocol, drm=input_stream_drm_version) - if is_helper.check_inputstream(): - link = unqoute_url(link) - debugLog("Restart Source Link: %s" % link) - headers = "User-Agent=%s&Content-Type=%s" % (Settings.userAgent(), input_stream_lic_content_type) - - if params.get('lic_url'): - lic_url = unqoute_url(params.get('lic_url')) - debugLog("Playing DRM protected Restart Stream") - debugLog("Restart License URL: %s" % lic_url) - streaming_url, play_item = scraper.liveStreamRestart(link, 'dash') - play_item.setContentLookup(False) - play_item.setMimeType(input_stream_mime) - play_item.setProperty('inputstream.adaptive.stream_headers', headers) - play_item.setProperty('inputstream', is_helper.inputstream_addon) - play_item.setProperty('inputstream.adaptive.manifest_type', input_stream_protocol) - play_item.setProperty('inputstream.adaptive.license_type', input_stream_drm_version) - play_item.setProperty('inputstream.adaptive.license_key', lic_url + '|' + headers +'|R{SSM}|') - else: - streaming_url, play_item = scraper.liveStreamRestart(link, 'hls') - debugLog("Playing Non-DRM protected Restart Stream") - play_item.setProperty('inputstreamaddon', 'inputstream.adaptive') - play_item.setProperty('inputstream.adaptive.stream_headers', headers) - play_item.setProperty('inputstream.adaptive.manifest_type', 'hls') - debugLog("Restart Stream Url: %s; play_item: %s" % (streaming_url, play_item)) - xbmc.Player().play(streaming_url, play_item) - else: - userNotification((translation(30066)).encode("utf-8")) - except Exception as e: - debugLog("Exception: %s" % ( e, ), xbmc.LOGINFO) - debugLog("TB: %s" % ( traceback.format_exc(), ), xbmc.LOGINFO) - userNotification((translation(30067)).encode("utf-8")) - elif mode == 'playlist': - startPlaylist(tvthekplayer, playlist) - elif mode == 'play': - link = "%s|User-Agent=%s" % (link, Settings.userAgent()) - play_item = xbmcgui.ListItem(path=link, offscreen=True) - xbmcplugin.setResolvedUrl(pluginhandle, True, listitem=play_item) - listCallback(False, pluginhandle) - elif mode == 'playDRM': - try: - import inputstreamhelper - stream_url = unqoute_url(params.get('link')) - lic_url = unqoute_url(params.get('lic_url')) - - is_helper = inputstreamhelper.Helper(input_stream_protocol, drm=input_stream_drm_version) - if is_helper.check_inputstream(): - debugLog("Video Url: %s" % stream_url) - debugLog("DRM License Url: %s" % lic_url) - play_item = xbmcgui.ListItem(path=stream_url, offscreen=True) - headers = "User-Agent=%s&Content-Type=%s" % (Settings.userAgent(), input_stream_lic_content_type) - - play_item.setContentLookup(False) - play_item.setMimeType(input_stream_mime) - play_item.setProperty('inputstream.adaptive.stream_headers', headers) - play_item.setProperty('inputstream', is_helper.inputstream_addon) - play_item.setProperty('inputstream.adaptive.manifest_type', input_stream_protocol) - play_item.setProperty('inputstream.adaptive.license_type', input_stream_drm_version) - play_item.setProperty('inputstream.adaptive.license_key', lic_url + '|' + headers + '|R{SSM}|') - xbmcplugin.setResolvedUrl(pluginhandle, True, listitem=play_item) - else: - userNotification((translation(30066)).encode("utf-8")) - listCallback(False, pluginhandle) - except: - userNotification((translation(30067)).encode("utf-8")) - elif mode == 'pvr': - channel = params.get('channel') - debugLog("Loading channel %s" % channel) - data = scraper.getLivestreamByChannel(channel) - if data: - video_url = "%s|User-Agent=%s" % (data['url'], Settings.userAgent()) - - if 'license' in data: - import inputstreamhelper - license = data['license'] - - is_helper = inputstreamhelper.Helper(input_stream_protocol, drm=input_stream_drm_version) - if is_helper.check_inputstream(): - debugLog("Video Url: %s" % video_url) - debugLog("DRM License Url: %s" % license) - play_item = xbmcgui.ListItem(path=video_url) - play_item.setLabel(data['title']) - play_item.setLabel2(channel) - play_item.setProperty('IsPlayable', 'true') - item_infos = { - 'title': data['title'], - 'plot': data['description'], - 'plotoutline': data['description'], - } - play_item.setInfo(type="Video", infoLabels=item_infos) - - if 'logo' in data: - item_art = { - 'clearlogo': data['logo'], - 'icon': data['logo'], - } - play_item.setArt(item_art) - - headers = "User-Agent=%s&Content-Type=%s" % (Settings.userAgent(), input_stream_lic_content_type) - - play_item.setContentLookup(False) - play_item.setMimeType(input_stream_mime) - play_item.setProperty('inputstream.adaptive.stream_headers', headers) - play_item.setProperty('inputstream', is_helper.inputstream_addon) - play_item.setProperty('inputstream.adaptive.manifest_type', input_stream_protocol) - play_item.setProperty('inputstream.adaptive.license_type', input_stream_drm_version) - play_item.setProperty('inputstream.adaptive.license_key', license + '|' + headers + '|R{SSM}|') - xbmcplugin.setResolvedUrl(pluginhandle, True, listitem=play_item) - else: - userNotification((translation(30066)).encode("utf-8")) - else: - play_item = xbmcgui.ListItem(path=video_url) - xbmcplugin.setResolvedUrl(pluginhandle, True, listitem=play_item) - elif sys.argv[2] == '': - getMainMenu() +@route_plugin.route('/episode/') +def get_episode(episodeid): + kodi_worker.log("Playing Episode %s" % episodeid, 'route') + videos = api.load_stream_data('/episode/%s' % episodeid) + kodi_worker.play(videos) + + +@route_plugin.route('/episode//more') +def get_show_from_episode(episodeid): + kodi_worker.log("Loading Shows from Episode %s" % episodeid, 'route') + other_episodes = api.get_related(episodeid) + for other_episode in other_episodes: + kodi_worker.render(other_episode) + kodi_worker.list_callback() + + +@route_plugin.route('/episode//segments') +def get_segements(episodeid): + kodi_worker.log("Playing Episode %s" % episodeid, 'route') + videos = api.load_stream_data('/episode/%s/segments?limit=500' % episodeid) + if kodi_worker.use_segments and kodi_worker.show_segments: + for video in videos: + kodi_worker.render(video) + kodi_worker.list_callback() else: - listCallback(False, pluginhandle) + kodi_worker.play(videos) + + +@route_plugin.route('/segment/') +def get_segement(segmentid): + kodi_worker.log("Playing Segment %s" % segmentid, 'route') + videos = api.load_stream_data('/segment/%s' % segmentid) + kodi_worker.play(videos) + + +@route_plugin.route('/videoitem/') +def get_videoitem(videoid): + kodi_worker.log("Playing Video %s" % videoid, 'route') + videos = api.load_stream_data('/videoitem/%s' % videoid) + kodi_worker.play(videos) + + +@route_plugin.route('/livestream/') +def get_livestream(videoid): + kodi_worker.log("Playing Livestream %s" % videoid, 'route') + videos = api.load_stream_data('/livestream/%s' % videoid) + kodi_worker.play(videos) + + +@route_plugin.route('/pvr/') +def get_pvr_livestream(channelreel): + kodi_worker.log("Playing PVR Livestream %s" % channelreel, 'route') + livestream = api.get_pvr(channelreel) + if livestream: + kodi_worker.play([livestream]) + + +@route_plugin.route('/recent') +def get_recently_added(): + videos = api.get_last_uploads() + for video in videos: + kodi_worker.render(video) + kodi_worker.list_callback() + + +@route_plugin.route('/schedule') +def get_schedule_selection(): + kodi_worker.log("Opening Schedule Selection", 'route') + items, filters = api.get_schedule_dates() + selected = kodi_worker.select_dialog(kodi_worker.get_translation(30130, 'Select a date'), items) + api.log(selected) + if selected is not False and selected > -1: + api.log("Loading %s Schedule" % filters[selected]) + request_url = api.api_endpoint_schedule % filters[selected] + target_url = kodi_worker.plugin.url_for_path(request_url) + kodi_worker.list_callback() + kodi_worker.execute('Container.Update(%s, replace)' % target_url) + else: + api.log("Canceled selection") + + +@route_plugin.route('/schedule/') +def get_schedule(scheduledate): + kodi_worker.log("Opening Schedule %s" % scheduledate, 'route') + request_url = api.api_endpoint_schedule % scheduledate + directories = api.get_url(request_url) + for directory in directories: + directory.annotate_channel() + directory.annotate_time() + kodi_worker.render(directory) + kodi_worker.list_callback() + + +@route_plugin.route('/search') +def get_search(): + kodi_worker.log("Opening Search History", 'route') + search_link = '/search/query' + search_dir = Directory(kodi_worker.get_translation(30131, 'Enter search ...', '%s ...'), "", search_link, translator=kodi_worker) + kodi_worker.render(search_dir) + directories = kodi_worker.get_stored_directories(search_history_file) + for directory in directories: + kodi_worker.render(directory) + kodi_worker.list_callback() + + +@route_plugin.route('/search/results/') +def get_search_results(query): + directories = api.get_search(query) + for directory in directories: + kodi_worker.render(directory) + kodi_worker.list_callback() + + +@route_plugin.route('/search-partial/
/') +def get_search_partial(section, query): + directories = api.get_search_partial(section, query, route_plugin.args) + for directory in directories: + kodi_worker.render(directory) + kodi_worker.list_callback() + + +@route_plugin.route('/search/query') +def get_search_dialog(): + kodi_worker.log("Opening Search Dialog", 'route') + query = kodi_worker.get_keyboard_input() + search_url = "/search/results/%s" % query + search_history_dir = Directory(query, "", search_url, translator=kodi_worker) + kodi_worker.list_callback() + if query and query.strip() != "": + kodi_worker.store_directory(search_history_dir, 'search_history') + target_url = kodi_worker.plugin.url_for_path(search_url) + else: + error_url = '/search' + target_url = kodi_worker.plugin.url_for_path(error_url) + kodi_worker.execute('Container.Update(%s, replace)' % target_url) + + +@route_plugin.route('/dialog/clear_search_history') +def clear_search_history(): + dialog = kodi_worker.get_progress_dialog(kodi_worker.get_translation(30132, 'Clearing search history')) + dialog.update(0, kodi_worker.get_translation(30133, 'Clearing ...', '%s ...')) + kodi_worker.clear_stored_directories(search_history_file) + dialog.update(100, kodi_worker.get_translation(30134, 'Done')) + dialog.close() + + +@route_plugin.route('/dialog/reload_cache') +def clear_cache(): + dialog = kodi_worker.get_progress_dialog('Reloading cache') + dialog.update(0, kodi_worker.get_translation(30136, 'Reloading cache ...', '%s ...')) + kodi_worker.log("Reloading channel/settings cache", 'route') + tmp_channel_map, tmp_channel_map_cached = kodi_worker.get_cached_file(channel_map_file) + tmp_settings, tmp_settings_cached = kodi_worker.get_cached_file(settings_file) + kodi_worker.remove_file(settings_file) + kodi_worker.remove_file(channel_map_file) + tmp_api = OrfOn(channel_map=tmp_channel_map, settings=tmp_settings, useragent=kodi_worker.useragent, kodi_worker=kodi_worker) + tmp_api.channel_map = False + tmp_api.settings = False + dialog.update(33, kodi_worker.get_translation(30137, 'Loading channels')) + tmp_channel_map = tmp_api.get_channel_map() + kodi_worker.save_json(tmp_channel_map, channel_map_file) + dialog.update(66, kodi_worker.get_translation(30138, 'Loading settings')) + tmp_settings = tmp_api.get_settings() + kodi_worker.save_json(tmp_settings, settings_file) + dialog.update(100, kodi_worker.get_translation(30134, 'Done')) + dialog.close() + + +@route_plugin.route('') +def get_url(url): + if re.search(r"^/https?://", url): + url = url[1:] + kodi_worker.log("Opening Video Url %s" % url, 'route') + kodi_worker.play_url(url) + else: + kodi_worker.log("Opening Generic Url %s" % url, 'route') + request_url = kodi_worker.build_url(url, route_plugin.args) + directories = api.get_url(request_url) + for directory in directories: + kodi_worker.render(directory) + kodi_worker.list_callback() + + +def run(): + route_plugin.run() diff --git a/resources/lib/Base.py b/resources/lib/Base.py deleted file mode 100644 index 85803d1..0000000 --- a/resources/lib/Base.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -import os -import re - -import simplejson as json -from kodi_six import xbmcplugin, xbmcgui, xbmcvfs -from kodi_six.utils import py2_encode, py2_decode -from . import Settings -from .Helpers import * - - -def showDialog(title, description): - xbmcgui.Dialog().notification(title, description, xbmcaddon.Addon().getAddonInfo('icon')) - - -def addDirectory(title, banner, backdrop, description, link, mode, pluginhandle): - parameters = {"link": link, "mode": mode} - u = build_kodi_url(parameters) - createListItem(title, banner, description, '', '', '', u, False, True, backdrop, pluginhandle, None) - - -def generateAddonVideoUrl(videourl): - videourl = buildLink(videourl) - return "plugin://%s/?mode=play&link=%s" % (xbmcaddon.Addon().getAddonInfo('id'), videourl) - - -def generateDRMVideoUrl(videourl, drm_lic_url): - parameters = {"link": videourl, "mode": "playDRM", "lic_url": drm_lic_url} - return build_kodi_url(parameters) - - -def buildLink(link): - link = link.replace("https://apasfpd.apa.at", "https://apasfpd.sf.apa.at") - if link: - return "%s|User-Agent=%s" % (link, Settings.userAgent()) - else: - return link - - -def createPlayAllItem(name, pluginhandle, stream_info=False): - play_all_parameters = {"mode": "playlist"} - play_all_url = build_kodi_url(play_all_parameters) - play_all_item = xbmcgui.ListItem(label=name, offscreen=True) - if stream_info: - description = stream_info['description'] - play_all_item.setArt({'thumb': stream_info['teaser_image'], 'poster': stream_info['teaser_image']}) - else: - description = "" - play_all_item.setInfo(type="Video", infoLabels={"Title": name, "Plot": description}) - xbmcplugin.addDirectoryItem(pluginhandle, play_all_url, play_all_item, isFolder=False, totalItems=-1) - - -def createListItem(title, banner, description, duration, date, channel, videourl, playable, folder, backdrop, pluginhandle, subtitles=None, blacklist=False, contextMenuItems=None): - contextMenuItems = contextMenuItems or [] - - liz = xbmcgui.ListItem(label=title, label2=channel, offscreen=True) - liz.setInfo(type="Video", infoLabels={"Title": title}) - liz.setInfo(type="Video", infoLabels={"Tvshowtitle": title}) - liz.setInfo(type="Video", infoLabels={"Sorttitle": title}) - liz.setInfo(type="Video", infoLabels={"Plot": description}) - liz.setInfo(type="Video", infoLabels={"Plotoutline": description}) - liz.setInfo(type="Video", infoLabels={"Aired": date}) - liz.setInfo(type="Video", infoLabels={"Studio": channel}) - liz.setProperty('fanart_image', backdrop) - liz.setProperty('IsPlayable', str(playable and not folder)) - liz.setArt({'thumb': banner, 'poster': banner, 'fanart': backdrop, "icon": banner}) - - if not folder: - liz.setInfo(type="Video", infoLabels={"mediatype": 'video'}) - videoStreamInfo = {'codec': 'h264', 'aspect': 1.78} - try: - videoStreamInfo.update({'duration': int(duration)}) - except (TypeError, ValueError): - debugLog("No Duration found in Video") - if videourl.lower().endswith('_qxb.mp4') or '_qxb' in videourl.lower(): - videoStreamInfo.update({'width': 1280, 'height': 720}) - if videourl.lower().endswith('_q8c.mp4') or '_q8c' in videourl.lower(): - videoStreamInfo.update({'width': 1280, 'height': 720}) - elif videourl.lower().endswith('_q6a.mp4') or '_q6a' in videourl.lower(): - videoStreamInfo.update({'width': 960, 'height': 540}) - elif videourl.lower().endswith('_q4a.mp4') or '_q4a' in videourl.lower(): - videoStreamInfo.update({'width': 640, 'height': 360}) - else: - videoStreamInfo.update({'width': 320, 'height': 180}) - liz.addStreamInfo('video', videoStreamInfo) - - liz.addStreamInfo('audio', {"codec": "aac", "language": "de", "channels": 2}) - if subtitles is not None and Settings.subtitles(): - if len(subtitles) > 0 and subtitles[0].endswith('.srt'): - subtitles.pop(0) - liz.addStreamInfo('subtitle', {"language": "de"}) - try: - liz.setSubtitles(subtitles) - except AttributeError: - # setSubtitles was introduced in Helix (v14) - # catch the error in Gotham (v13) - pass - - if blacklist: - match = re.search(r'( - \w\w, \d\d.\d\d.\d\d\d\d)', title) - if match is not None: - bltitle = title.split(" - ") - bltitle = bltitle[0].split(": ") - - bl_title = bltitle[0].replace("+", " ").strip() - else: - bl_title = title.replace("+", " ").strip() - - blparameters = {"mode": "blacklistShow", "link": bl_title} - blurl = build_kodi_url(blparameters) - contextMenuItems.append(('%s %s %s' % (Settings.localizedString(30038).encode("utf-8"), bl_title, Settings.localizedString(30042).encode("utf-8")), 'XBMC.RunPlugin(%s)' % blurl)) - if checkBlacklist(bl_title): - return - - liz.addContextMenuItems(contextMenuItems) - xbmcplugin.addDirectoryItem(pluginhandle, url=videourl, listitem=liz, isFolder=folder) - return liz - - -def checkBlacklist(title): - addonUserDataFolder = xbmcvfs.translatePath("special://profile/addon_data/plugin.video.orftvthek") - bl_json_file = os.path.join(addonUserDataFolder, 'blacklist.json') - if os.path.exists(bl_json_file): - if os.path.getsize(bl_json_file) > 0: - data = getJsonFile(bl_json_file) - tmp = data - for item in tmp: - if py2_decode(item) == py2_decode(title): - return True - return False - - -def removeBlacklist(title): - addonUserDataFolder = xbmcvfs.translatePath("special://profile/addon_data/plugin.video.orftvthek") - bl_json_file = os.path.join(addonUserDataFolder, 'blacklist.json') - if os.path.exists(bl_json_file): - if os.path.getsize(bl_json_file) > 0: - data = getJsonFile(bl_json_file) - tmp = data - for item in tmp: - if item.encode('UTF-8') == title: - tmp.remove(item) - saveJsonFile(tmp, bl_json_file) - - -def printBlacklist(banner, backdrop, translation, pluginhandle): - addonUserDataFolder = xbmcvfs.translatePath("special://profile/addon_data/plugin.video.orftvthek") - bl_json_file = os.path.join(addonUserDataFolder, 'blacklist.json') - if os.path.exists(bl_json_file): - if os.path.getsize(bl_json_file) > 0: - data = getJsonFile(bl_json_file) - for item in data: - item = item.encode('UTF-8') - description = translation(30040).encode('UTF-8') % item - parameters = {'link': item, 'mode': 'unblacklistShow'} - url = build_kodi_url(parameters) - createListItem(item, banner, description, None, None, None, url, False, False, backdrop, pluginhandle) - - -def saveJsonFile(data, file): - with open(file, 'w') as data_file: - data_file.write(json.dumps(data, 'utf-8')) - data_file.close() - - -def getJsonFile(file): - with open(file, 'r') as data_file: - data = json.load(data_file, 'UTF-8') - return data - - -def blacklistItem(title): - addonUserDataFolder = xbmcvfs.translatePath("special://profile/addon_data/plugin.video.orftvthek") - bl_json_file = os.path.join(addonUserDataFolder, 'blacklist.json') - title = unqoute_url(title) - title = title.replace("+", " ").strip() - # check if file exists - if os.path.exists(bl_json_file): - # check if file already has an entry - if os.path.getsize(bl_json_file) > 0: - # append value to JSON File - if not checkBlacklist(title): - data = getJsonFile(bl_json_file) - data.append(title) - saveJsonFile(data, bl_json_file) - # found empty file - writing first record - else: - data = [] - data.append(title) - saveJsonFile(data, bl_json_file) - # create json file - else: - if not os.path.exists(addonUserDataFolder): - os.makedirs(addonUserDataFolder) - data = [] - data.append(title) - saveJsonFile(data, bl_json_file) - - -def unblacklistItem(title): - title = unqoute_url(title) - title = title.replace("+", " ").strip() - removeBlacklist(title) - - -def isBlacklisted(title): - title = unqoute_url(title) - title = py2_decode(title.replace("+", " ").strip()) - return checkBlacklist(title) - - -def searchHistoryPush(title): - addonUserDataFolder = xbmcvfs.translatePath("special://profile/addon_data/plugin.video.orftvthek") - json_file = os.path.join(addonUserDataFolder, 'searchhistory.json') - title = unqoute_url(title) - title = title.replace("+", " ").strip() - # check if file exists - if os.path.exists(json_file): - # check if file already has an entry - if os.path.getsize(json_file) > 0: - # append value to JSON File - data = getJsonFile(json_file) - data.append(title) - saveJsonFile(data, json_file) - # found empty file - writing first record - else: - data = [] - data.append(title) - saveJsonFile(data, json_file) - # create json file - else: - if not os.path.exists(addonUserDataFolder): - os.makedirs(addonUserDataFolder) - data = [] - data.append(title) - saveJsonFile(data, json_file) - -def searchHistoryGet(): - addonUserDataFolder = xbmcvfs.translatePath("special://profile/addon_data/plugin.video.orftvthek") - json_file = os.path.join(addonUserDataFolder, 'searchhistory.json') - if os.path.exists(json_file): - if os.path.getsize(json_file) > 0: - data = getJsonFile(json_file) - return data - return [] \ No newline at end of file diff --git a/resources/lib/Common.py b/resources/lib/Common.py deleted file mode 100644 index 456525c..0000000 --- a/resources/lib/Common.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" - Parsedom for XBMC plugins - Copyright (C) 2010-2011 Tobias Ussing And Henrik Mosgaard Jensen - - 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 3 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, see . -""" - -from kodi_six.utils import py2_encode, py2_decode -from future.builtins import str -from future.builtins import range -import sys -import xbmc - -PY3 = sys.version_info.major >=3 -if PY3: - from urllib.parse import unquote, urlencode - from urllib.request import urlopen as OpenRequest - from urllib.request import Request as HTTPRequest - from urllib.error import HTTPError - from html.parser import HTMLParser - from html import unescape -else: - from urllib import unquote, urlencode - from urllib2 import urlopen as OpenRequest - from urllib2 import Request as HTTPRequest - from urllib2 import HTTPError - from HTMLParser import HTMLParser - parser = HTMLParser() - unescape = parser.unescape - -import re - -USERAGENT = u"Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1" -DEBUG_SCRAPER = False - -def replaceHTMLCodes(txt): - log(repr(txt)) - txt = re.sub('(&#[0-9]+)([^;^0-9]+)', '\\1;\\2', txt) - txt = unescape(txt) - txt = txt.replace("&", "&") - log(repr(txt)) - return txt - - -def stripTags(html): - log(repr(html)) - sub_start = html.find("<") - sub_end = html.find(">") - while sub_end > sub_start > -1: - html = html.replace(html[sub_start:sub_end + 1], "").strip() - sub_start = html.find("<") - sub_end = html.find(">") - - log(repr(html)) - return html - - -def _getDOMContent(html, name, match, ret): # Cleanup - log("match: " + match) - - endstr = u"" - - start = html.find(match) - end = html.find(endstr, start) - pos = html.find("<" + name, start + 1 ) - - log(str(start) + " < " + str(end) + ", pos = " + str(pos) + ", endpos: " + str(end)) - - while pos < end and pos != -1: # Ignore too early return - tend = html.find(endstr, end + len(endstr)) - if tend != -1: - end = tend - pos = html.find("<" + name, pos + 1) - log("loop: " + str(start) + " < " + str(end) + " pos = " + str(pos)) - - log("start: %s, len: %s, end: %s" % (start, len(match), end)) - if start == -1 and end == -1: - result = u"" - elif start > -1 and end > -1: - result = html[start + len(match):end] - elif end > -1: - result = html[:end] - elif start > -1: - result = html[start + len(match):] - - if ret: - endstr = html[end:html.find(">", html.find(endstr)) + 1] - result = match + result + endstr - - log("done result length: " + str(len(result))) - return result - -def _getDOMAttributes(match, name, ret): - lst = re.compile('<' + name + '.*?' + ret + '=([\'"].[^>]*?[\'"])>', re.M | re.S).findall(match) - if len(lst) == 0: - lst = re.compile('<' + name + '.*?' + ret + '=(.[^>]*?)>', re.M | re.S).findall(match) - ret = [] - for tmp in lst: - cont_char = tmp[0] - if cont_char in "'\"": - log("Using %s as quotation mark" % cont_char) - - # Limit down to next variable. - if tmp.find('=' + cont_char, tmp.find(cont_char, 1)) > -1: - tmp = tmp[:tmp.find('=' + cont_char, tmp.find(cont_char, 1))] - - # Limit to the last quotation mark - if tmp.rfind(cont_char, 1) > -1: - tmp = tmp[1:tmp.rfind(cont_char)] - else: - log("No quotation mark found") - if tmp.find(" ") > 0: - tmp = tmp[:tmp.find(" ")] - elif tmp.find("/") > 0: - tmp = tmp[:tmp.find("/")] - elif tmp.find(">") > 0: - tmp = tmp[:tmp.find(">")] - - ret.append(tmp.strip()) - - log("Done: " + repr(ret)) - return ret - -def _getDOMElements(item, name, attrs): - lst = [] - for key in attrs: - lst2 = re.compile('(<' + name + '[^>]*?(?:' + key + '=[\'"]' + attrs[key] + '[\'"].*?>))', re.M | re.S).findall(item) - if len(lst2) == 0 and attrs[key].find(" ") == -1: # Try matching without quotation marks - lst2 = re.compile('(<' + name + '[^>]*?(?:' + key + '=' + attrs[key] + '.*?>))', re.M | re.S).findall(item) - if len(lst) == 0: - log("Setting main list " + repr(lst2)) - lst = lst2 - lst2 = [] - else: - log("Setting new list " + repr(lst2)) - test = list(range(len(lst))) - test.reverse() - for i in test: # Delete anything missing from the next list. - if not lst[i] in lst2: - log("Purging mismatch " + str(len(lst)) + " - " + repr(lst[i])) - del(lst[i]) - - if len(lst) == 0 and attrs == {}: - log("No list found, trying to match on name only") - lst = re.compile('(<' + name + '>)', re.M | re.S).findall(item) - if len(lst) == 0: - lst = re.compile('(<' + name + ' .*?>)', re.M | re.S).findall(item) - - log("Done: " + str(type(lst))) - return lst - - -def parseDOM(html, name=u"", attrs={}, ret=False): - log("Name: " + repr(name) + " - Attrs:" + repr(attrs) + " - Ret: " + repr(ret) + " - HTML: " + str(type(html))) - ret = py2_decode(ret) - if isinstance(name, str): - try: - name = name - except: - log("Couldn't decode name binary string: " + repr(name)) - - if isinstance(html, str): - try: - html = [py2_decode(html)] - except: - log("Couldn't decode html binary string. Data length: " + repr(len(html))) - html = [html] - elif isinstance(html, bytes): - html = [html.decode('ascii')] - elif str(type(html)) == "": - html = [str(html)] - elif not isinstance(html, list): - log("Input isn't list or string/unicode.") - return u"" - - if not name.strip(): - log("Missing tag name") - return u"" - - ret_lst = [] - for item in html: - temp_item = re.compile('(<[^>]*?\n[^>]*?>)').findall(item) - for match in temp_item: - item = item.replace(match, match.replace("\n", " ")) - - lst = _getDOMElements(item, name, attrs) - - if isinstance(ret, str): - log("Getting attribute %s content for %s matches " % (ret, len(lst))) - lst2 = [] - for match in lst: - lst2 += _getDOMAttributes(match, name, ret) - lst = lst2 - else: - log("Getting element content for %s matches " % len(lst)) - lst2 = [] - for match in lst: - log("Getting element content for %s" % match) - temp = _getDOMContent(item, name, match, ret).strip() - item = item[item.find(temp, item.find(match)) + len(temp):] - lst2.append(temp) - lst = lst2 - ret_lst += lst - - log("Done: " + repr(ret_lst)) - return ret_lst - -def fetchPage(params={}): - get = params.get - link = get("link") - ret_obj = {} - if get("post_data"): - log("called for : " + repr(params['link'])) - else: - log("called for : " + repr(params)) - - if not link or int(get("error", "0")) > 2: - log("giving up") - ret_obj["status"] = 500 - return ret_obj - - if get("post_data"): - if get("hide_post_data"): - log("Posting data") - else: - log("Posting data: " + urlencode(get("post_data"))) - - request = HTTPRequest(link, urlencode(get("post_data"))) - request.add_header('Content-Type', 'application/x-www-form-urlencoded') - else: - log("Got request") - request = HTTPRequest(link) - - if get("headers"): - for head in get("headers"): - request.add_header(head[0], head[1]) - - request.add_header('User-Agent', USERAGENT) - - if get("cookie"): - request.add_header('Cookie', get("cookie")) - - if get("refering"): - request.add_header('Referer', get("refering")) - - try: - log("connecting to server...") - - con = OpenRequest(request) - ret_obj["header"] = con.info() - ret_obj["new_url"] = con.geturl() - if get("no-content", "false") == u"false" or get("no-content", "false") == "false": - inputdata = con.read() - ret_obj["content"] = inputdata.decode("utf-8") - - con.close() - - log("Done") - ret_obj["status"] = 200 - return ret_obj - - except HTTPError as e: - err = str(e) - log("HTTPError : " + err) - log("HTTPError - Headers: " + str(e.headers) + " - Content: " + e.fp.read()) - - params["error"] = str(int(get("error", "0")) + 1) - ret = fetchPage(params) - - if not "content" in ret and e.fp: - ret["content"] = e.fp.read() - return ret - - ret_obj["status"] = 500 - return ret_obj - - -def log(msg): - if DEBUG_SCRAPER: - output = py2_encode(msg) - xbmc.log(output, xbmc.LOGDEBUG) diff --git a/resources/lib/Directory.py b/resources/lib/Directory.py new file mode 100644 index 0000000..5e580b7 --- /dev/null +++ b/resources/lib/Directory.py @@ -0,0 +1,428 @@ +import re +from datetime import datetime, timedelta + + +class Directory: + def __init__(self, title, description, link, content_id="", content_type="", thumbnail="", backdrop="", poster="", source={}, translator=None, proxy=False): + self.translator = translator + self.title = title + if description: + self.description = description.strip() + else: + self.description = " " + self.link = link + self.content_id = content_id + self.content_type = content_type + self.thumbnail = thumbnail + self.backdrop = backdrop + self.poster = poster + self.meta = self.build_meta(source) + self.source = source + self.videos = {} + self.context_menu = self.build_content_menu() + self.pvr_mode = False + + def has_segments(self): + seg_matcher = r"\/episode\/[1-10].*\/segments" + if re.search(seg_matcher, self.link): + return True + return False + + def build_content_menu(self) -> list: + context_menu_items = [] + + if self.is_livestream(): + restart_url = self.get_restart() + if restart_url: + context_menu_items.append({ + 'title': self.translate_string(30139, 'Restart'), + 'url': "restart/%s" % self.source.get('id'), + 'type': 'run' + }) + + if self.type() == 'episode': + context_menu_items.append({ + 'title': self.translate_string(30140, 'All episodes'), + 'url': "episode/%s/more" % self.source.get('id'), + 'type': 'update' + }) + + if self.type() == 'segment' and self.source.get('episode_id'): + context_menu_items.append({ + 'title': self.translate_string(30140, 'All episodes'), + 'url': "episode/%s/more" % self.source.get('episode_id'), + 'type': 'update' + }) + + if 'genre_id' in self.source and 'id' in self.source: + related_link = "/lane/related_content/%s/%s" % (self.source.get('genre_id'), self.source.get('id')) + context_menu_items.append({ + 'title': self.translate_string(30150, 'Related content'), + 'url': related_link, + 'type': 'update' + }) + + return context_menu_items + + def get_context_menu(self) -> list: + return self.context_menu + + def translate_string(self, translation_id, fallback, replace=None): + if self.translator: + return self.translator.get_translation(translation_id, fallback, replace) + else: + return fallback + + @staticmethod + def build_meta(item) -> dict: + meta = {} + if 'online_episode_count' in item: + meta['episodes'] = item['online_episode_count'] + + # Show Meta + if 'genre_title' in item and item['genre_title'] is not None: + if item['genre_title'] == 'Film & Serie' and 'sub_headline' in item and item['sub_headline'] is not None: + meta['genre'] = item['sub_headline'] + else: + meta['genre'] = item['genre_title'] + + if 'genre_id' in item and item['genre_id'] is not None: + meta['genre_id'] = item['genre_id'] + if 'production_year' in item and item['production_year'] is not None: + meta['year'] = item['production_year'] + if 'production_country' in item and item['production_country'] is not None: + meta['country'] = item['production_country'] + + # Build Release Infos + if 'date' in item and item['date'] is not None: + meta['release_date'] = item['date'] + elif 'episode_date' in item and item['episode_date'] is not None: + meta['release_date'] = item['episode_date'] + elif 'updated_at' in item and item['updated_at'] is not None: + meta['release_date'] = item['updated_at'] + + # Build additional Title Infos + if 'headline' in item and item['headline'] is not None: + meta['headline'] = item['headline'] + if 'sub_headline' in item and item['sub_headline'] is not None: + meta['sub_headline'] = item['sub_headline'] + if 'episode_title' in item and item['episode_title'] is not None: + meta['episode'] = item['episode_title'] + + # Build Channel Info + if 'main_channel_id' in item and item['main_channel_id'] is not None: + if str(item['main_channel_id']) in item['channel_meta']: + meta['channel'] = item['channel_meta'][str(item['main_channel_id'])] + meta['channel_id'] = item['main_channel_id'] + elif 'channel_id' in item and item['channel_id'] is not None: + if str(item['channel_id']) in item['channel_meta']: + meta['channel'] = item['channel_meta'][str(item['channel_id'])] + meta['channel_id'] = item['channel_id'] + elif 'SSA' in item and 'channel' in item['SSA']: + for channel in item['channel_meta']: + if item['channel_meta'][channel]['reel'] == item['SSA']['channel']: + meta['channel'] = item['channel_meta'][channel] + break + + # Build Accessibility infos + if 'audio_description_service_available' in item and item['audio_description_service_available'] is not None: + meta['audio_description_available'] = item['audio_description_service_available'] + if 'has_subtitle' in item and item['has_subtitle'] is not None: + meta['subtitles'] = item['has_subtitle'] + else: + meta['subtitles'] = False + + # Stream Meta + if 'two_channel_audio' in item and item['two_channel_audio'] is not None: + meta['multiaudio'] = item['two_channel_audio'] + if 'restart' in item and item['restart'] is not None: + meta['restart'] = item['restart'] + if 'uhd' in item and item['uhd'] is not None: + meta['uhd'] = item['uhd'] + if 'duration_seconds' in item and item['duration_seconds'] is not None: + meta['duration'] = int(item['duration_seconds']) + if 'audio_description' in item and item['audio_description'] is not None: + meta['audio_description'] = item['audio_description'] + elif 'audio_description_service_available' in item and item.get('title').startswith("AD | "): + meta['audio_description'] = True + else: + meta['audio_description'] = False + if 'oegs' in item and item['oegs'] is not None: + meta['oegs'] = item['oegs'] + elif 'is_oegs' in item and item['is_oegs'] is not None: + meta['oegs'] = item['is_oegs'] + else: + meta['oegs'] = False + + if 'right' in item and item['right'] is not None and item['right'] == 'austria': + meta['geo_lock'] = True + else: + meta['geo_lock'] = False + return meta + + def label(self) -> str: + if self.meta.get('headline') and self.meta.get('headline') != self.title and self.meta.get('headline') not in self.title: + label = "%s | %s" % (self.title, self.meta.get('headline')) + else: + label = self.title + + if self.is_livestream() and self.get_channel() and not self.is_pvr_mode(): + channel = self.get_channel() + if self.meta.get('restart'): + label = "[LIVE] [R] [%s] %s" % (channel, label) + else: + label = "[LIVE] [%s] %s" % (channel, label) + elif self.is_pvr_mode(): + channel = self.get_channel() + label = "%s | %s" % (channel, label) + return label + + def label2(self) -> str: + if self.meta.get('sub_headline') and self.meta.get('sub_headline') != self.title and self.meta.get('sub_headline') not in self.title: + return self.meta.get('sub_headline') + + def is_livestream(self) -> bool: + return self.type() == 'timeshift' or self.type() == 'livestream' + + def livestream_active(self) -> bool: + current_time = datetime.now() + start_time = self.get_start_time() + end_time = self.get_end_time() + if start_time < current_time < end_time: + return True + + def is_geo_locked(self): + return self.meta.get('geo_lock') + + def has_audio_description(self): + return self.meta.get('audio_description') + + def has_sign_language(self): + return self.meta.get('oegs') + + def get_start_time(self): + return datetime.fromisoformat(self.get_source().get('start')).replace(tzinfo=None) + + def get_start_time_iso(self): + ref_date = datetime.fromisoformat(self.get_source().get('start')) - timedelta(hours=2) + d_date = ref_date.strftime("%Y%m%d") + d_time = ref_date.strftime("%H%M%S") + return "%sT%s" % (d_date, d_time) + + def get_end_time(self): + return datetime.fromisoformat(self.get_source().get('end')).replace(tzinfo=None) + + def set_channel(self, channel_reel): + for channel in self.source['channel_meta']: + if self.source['channel_meta'][channel]['reel'] == channel_reel: + self.meta['channel'] = self.source['channel_meta'][channel] + break + + def has_timeshift(self): + if 'timeshift_available_livestream' in self.source and 'video_type' in self.source: + if self.source['timeshift_available_livestream'] and self.source['video_type'] == 'timeshift': + return True + return False + + def get_restart(self): + if 'restart' in self.source: + if self.source['restart'] and '_embedded' in self.source and 'channel' in self.source['_embedded']: + restart_url = self.source['_embedded']['channel']['channel_restart_url_hbbtv'] + return restart_url + return False + + def get_channel(self): + special_regex = r"^web\d*" + if self.meta.get('channel'): + if re.search(special_regex, self.meta.get('channel').get('name')): + return "Special" + return self.meta.get('channel').get('name') + + def get_channel_logo(self): + if self.meta.get('channel'): + return self.meta.get('channel').get('logo') + + def get_resolution(self): + if self.meta.get('uhd'): + return 3840, 2160 + else: + return 1280, 720 + + def set_stream(self, sources): + self.videos = sources + + def get_stream(self): + return self.videos + + def date(self): + return self.meta.get('release_date') + + def time(self): + try: + return datetime.fromisoformat(self.date()).strftime("%H:%M") + except TypeError: + self.log('No time set for %s' % self.label()) + + def get_description(self) -> str: + if self.description is not None: + return self.description + else: + return "" + + def get_meta_description(self): + meta_description = {} + if not self.is_pvr_mode(): + if self.get_episodes() > 1: + meta_description[self.translate_string(30141, 'Episodes')] = self.get_episodes() + if self.get_channel(): + meta_description[self.translate_string(30142, 'Channel')] = self.get_channel() + if self.is_livestream() and self.get_stream_runtime(): + meta_description[self.translate_string(30113, 'Livestream')] = self.get_stream_runtime() + if not self.livestream_active(): + meta_description[self.translate_string(30114, 'Starts in')] = "%d min" % self.get_stream_start_delta() + if 'episode_title' in self.get_source() and 'sub_headline' in self.get_source(): + meta_description[self.meta.get('sub_headline')] = "" + + return meta_description + + def get_stream_start_delta(self): + current_time = datetime.now() + start_time = self.get_start_time() + return int((start_time - current_time).total_seconds() / 60) + + def get_stream_runtime(self): + start_time = self.get_start_time().strftime("%H:%M") + end_time = self.get_start_time().strftime("%H:%M") + if start_time != end_time: + return "%s - %s" % (start_time, end_time) + + def annotate_time(self): + self.title = "%s | %s" % (self.time(), self.title) + + def annotate_channel(self): + self.title = "[%s] %s" % (self.get_channel(), self.title) + + def country(self): + return self.meta.get('country') + + def year(self): + return self.meta.get('year') + + def genre(self) -> str: + if 'genre' in self.meta: + return self.meta.get('genre') + + def has_art(self) -> bool: + if self.poster or self.thumbnail or self.backdrop: + return True + return False + + def is_playable(self) -> bool: + if self.type() == 'episode': + return True + if self.type() == 'segment': + return True + if self.is_livestream(): + return True + if self.get_episodes() == 1 and self.type() == 'temporary': + return True + return False + + def get_episodes(self): + if self.meta.get('episodes') is not None: + return int(self.meta.get('episodes')) + return 1 + + def get_cast(self): + cast = [] + part = None + cast_extract_pattern = r'(?P.*?)(\s\((?P.*?)\)|,|u.v.m| u.a.|u. a.)' + if 'Besetzung:' in self.description: + part = self.description.split('Besetzung:') + elif 'Hauptdarsteller:' in self.description: + part = self.description.split('Hauptdarsteller:') + elif 'Besetzung\r\n' in self.description: + part = self.description.split('Besetzung\r\n') + elif 'Hauptdarsteller\r\n' in self.description: + part = self.description.split('Hauptdarsteller\r\n') + elif 'Mit:' in self.description: + part = self.description.split('Mit:') + elif '\r\nMit ' in self.description: + part = self.description.split('\r\nMit ') + + try: + if part is not None and len(part) > 1: + matches = re.findall(cast_extract_pattern, part[1], re.DOTALL) + for name, dirty_role, role in matches: + if name.strip() != "": + if '\r\n' in name.strip() or 'Regie:' in name.strip(): + break + if role.strip() != "": + cast.append((name.strip(), role.strip())) + else: + cast.append(name.strip()) + return cast + except re.error as e: + return cast + + def url(self) -> str: + return self.link + + def set_url(self, url): + self.link = url + + def type(self) -> str: + return self.content_type + + def get_duration(self): + if self.meta.get('duration') is not None: + return int(self.meta.get('duration')) + + def is_pvr_mode(self): + return self.pvr_mode + + def set_pvr_mode(self): + self.pvr_mode = True + + def media_type(self) -> str: + contenttype = self.type() + if self.label2() == 'Fernsehfilm': + return 'movie' + if self.get_duration() is not None and self.get_duration() > 60 * 60: + return 'movie' + if contenttype == 'lane': + return 'video' + if contenttype == 'episode': + return 'episode' + if contenttype == 'segment': + return 'episode' + if contenttype == 'temporary': + return 'tvshow' + return 'movie' + + def get_source(self): + return self.source + + def debug(self): + self.log('Title: %s' % self.title) + self.log('Playable: %s' % self.is_playable()) + self.log('Description: %s' % self.description) + self.log('Link: %s' % self.link) + self.log('ID: %s' % self.content_id) + self.log('Type: %s' % self.content_type) + self.log('Playable: %s' % self.is_playable()) + self.log('Thumbnail: %s' % self.thumbnail) + self.log('Backdrop: %s' % self.backdrop) + self.log('Poster: %s' % self.poster) + for item in self.meta: + self.log("%s: %s" % (item.capitalize().replace("_", " "), self.meta[item])) + + for context_menu_item in self.context_menu: + self.log('Context Menu Item: %s' % context_menu_item.get('title')) + + if len(self.get_stream()): + self.log('Stream Data available %d' % len(self.get_stream())) + + def log(self, msg, msg_type='info'): + if self.translator: + self.translator.log("[%s][ORFON][DIRECTORY] %s" % (msg_type.upper(), msg)) diff --git a/resources/lib/Helpers.py b/resources/lib/Helpers.py deleted file mode 100644 index cc0165d..0000000 --- a/resources/lib/Helpers.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -from kodi_six.utils import py2_encode, py2_decode - -import xbmc -import xbmcgui -import xbmcaddon -import sys - -PY3 = sys.version_info.major >=3 -if PY3: - from urllib.parse import unquote, urlencode - from urllib.request import urlopen as OpenRequest - from urllib.request import Request as HTTPRequest - from urllib.error import HTTPError -else: - from urllib import unquote, urlencode - from urllib2 import HTTPError - from urllib2 import urlopen as OpenRequest - from urllib2 import Request as HTTPRequest - - -def unqoute_url(url): - try: - return unquote(url) - except: - return unquote(url.encode('utf-8')) - - -def build_kodi_url(parameters): - return sys.argv[0] + '?' + encode_parameters(parameters) - - -def encode_parameters(parameters): - parameters = { k: v if v is not None else "" - for k, v in parameters.items() } - try: - return urlencode(parameters) - except: - parameters = {k: unicode(v).encode("utf-8") for k, v in list(parameters.items())} - return urlencode(parameters) - - -def url_get_request(url, authorization=False): - if authorization: - request = HTTPRequest(url) - request.add_header('Authorization', 'Basic %s' % authorization) - else: - request = url - return OpenRequest(request) - - -def parameters_string_to_dict(parameters): - paramDict = {} - if parameters: - paramPairs = parameters[1:].split("&") - for paramsPair in paramPairs: - paramSplits = paramsPair.split('=') - if (len(paramSplits)) == 2: - paramDict[paramSplits[0]] = paramSplits[1] - return paramDict - - -def debugLog(message, loglevel=xbmc.LOGDEBUG): - output = py2_encode(message) - xbmc.log(msg=output, level=loglevel) - -def userNotification(message, title="ORF TVThek"): - output = py2_encode(message) - xbmcgui.Dialog().notification(title, output, icon=xbmcgui.NOTIFICATION_ERROR) diff --git a/resources/lib/HtmlScraper.py b/resources/lib/HtmlScraper.py deleted file mode 100644 index 84b7923..0000000 --- a/resources/lib/HtmlScraper.py +++ /dev/null @@ -1,1257 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -import datetime -from .Common import * - -from .Base import * -from .Scraper import * - - -class htmlScraper(Scraper): - __urlBase = 'https://tvthek.orf.at' - __urlLive = __urlBase + '/live' - __urlMostViewed = __urlBase + '/most-viewed' - __urlNewest = __urlBase + '/newest' - __urlSchedule = __urlBase + '/schedule' - __urlSearch = __urlBase + '/search' - __urlShows = __urlBase + '/profiles' - __urlTips = __urlBase + '/tips' - __urlFocus = __urlBase + '/in-focus' - __urlTopics = __urlBase + '/topics' - __urlTopicLane = __urlBase + '/lane/topic/' - __urlArchive = __urlBase + '/history' - __urlTrailer = __urlBase + '/coming-soon' - - __videoQualities = ["Q1A", "Q4A", "Q6A", "Q8C", "QXB", "QXA"] - - _bundeslandMap = { - 'orf2b': 'Burgenland', - 'orf2stmk': 'Steiermark', - 'orf2w': 'Wien', - 'orf2ooe': 'Oberösterreich', - 'orf2k': 'Kärnten', - 'orf2n': 'Niederösterreich', - 'orf2s': 'Salzburg', - 'orf2v': 'Vorarlberg', - 'orf2t': 'Tirol', - } - - def __init__(self, xbmc, settings, pluginhandle, quality, protocol, delivery, defaultbanner, defaultbackdrop, usePlayAllPlaylist): - self.translation = settings.getLocalizedString - self.xbmc = xbmc - self.videoQuality = quality - self.videoDelivery = delivery - self.videoProtocol = protocol - self.pluginhandle = pluginhandle - self.defaultbanner = defaultbanner - self.defaultbackdrop = defaultbackdrop - self.enableBlacklist = settings.getSetting("enableBlacklist") == "true" - self.usePlayAllPlaylist = usePlayAllPlaylist - debugLog('HTML Scraper - Init done') - - def getLivestreamByChannel(self, channel): - html = fetchPage({'link': self.__urlLive}) - wrapper = parseDOM(html.get("content"), name='div', attrs={'class': 'b-livestream-per-channel'}) - channels = parseDOM(wrapper, name='div', attrs={'class': 'b-lane.*?'}) - - for result in channels: - channel_detail_url = False - channel_logo = parseDOM(result, name='img', attrs={'class': 'channel-logo'}, ret='src') - channel_id = re.findall(r'[^\/]+(?=\.)', str(channel_logo))[0].replace('-logo', '') - channel_program = parseDOM(result, name='a', attrs={'class': 'js-link-box'}, ret='title')[0] - - if channel in self._bundeslandMap: - channel = 'orf2' - bundesland_article = parseDOM(result, name='li', attrs={'class': '.*?is-bundesland-heute.*?'}, ret='data-jsb') - if len(bundesland_article): - bundesland_data = replaceHTMLCodes(bundesland_article[0]) - bundesland_data = json.loads(bundesland_data) - for bundesland_item_key in bundesland_data: - bundesland_item = bundesland_data.get(bundesland_item_key) - if bundesland_item and bundesland_item is not True and len(bundesland_item): - if self._bundeslandMap[channel] == bundesland_item.get('bundesland'): - channel_detail_url = bundesland_item.get('url') - - if not channel_detail_url and channel_id == channel: - channel_detail_url = parseDOM(result, name='a', attrs={'class': 'js-link-box'}, ret='href')[0] - - if channel_detail_url: - channel_html = fetchPage({'link': channel_detail_url}) - player = parseDOM(channel_html.get("content"), name='div', attrs={'class': "player_viewport.*?"}) - player_meta = parseDOM(channel_html.get("content"), name='section', attrs={'class': "b-video-details.*?"}) - if len(player): - data = parseDOM(player[0], name='div', attrs={}, ret="data-jsb") - channel_description = parseDOM(player_meta[0], name='p', attrs={'class': "description-text.*?"})[0] - license_url = self.getLivestreamDRM(data) - video_url = self.getLivestreamUrl(data, self.videoQuality) - # Remove Get Parameters because InputStream Adaptive cant handle it. - video_url = re.sub(r"\?[\S]+", '', video_url, 0) - - uhd_25_video_url = self.getLivestreamUrl(data, 'UHD', True) - if uhd_25_video_url: - uhd_50_video_url = uhd_25_video_url.replace('_uhd_25/', '_uhd_50/') - video_url = uhd_50_video_url - if license_url: - return {'title': channel_program, 'description': channel_description, 'url': video_url,'license': license_url} - else: - return {'title': channel_program, 'description': channel_description, 'url': video_url} - return [] - - def getMostViewed(self): - self.getTeaserList(self.__urlMostViewed, "b-teasers-list") - - def getNewest(self): - self.getTeaserList(self.__urlNewest, "b-teasers-list") - - def getTips(self): - self.getTeaserList(self.__urlTips, "b-teasers-list") - - # Parses the Frontpage Carousel - def getHighlights(self): - self.getTeaserSlideshow(self.__urlBase) - self.getTeaserList(self.__urlBase, "stage-subteaser-list") - - def getTrailers(self): - self.getTeaserList(self.__urlTrailer, "b-teasers-list") - - def getFocus(self): - self.getLaneTopicOverview(self.__urlFocus) - - # Extracts VideoURL from JSON String - def getVideoUrl(self, sources, drm_license=None): - for source in sources: - if drm_license and source['quality'].lower()[0:3] == self.videoQuality.lower() and source['delivery'].lower() == 'dash': - debugLog("Found DRM Video Url %s" % source["src"]) - return generateDRMVideoUrl(source["src"], drm_license) - elif source["protocol"].lower() == self.videoProtocol.lower() and source["delivery"].lower() == self.videoDelivery.lower() and source["quality"].lower() == self.videoQuality.lower(): - debugLog("Found Simple Video Url %s" % source["src"]) - return generateAddonVideoUrl(source["src"]) - return False - - # Parses teaser lists - def getTeaserList(self, url, list_class, list_type="ul"): - url = unqoute_url(url) - html = fetchPage({'link': url}) - container = parseDOM(html.get("content"), name='main', attrs={'class': "main"}, ret=False) - teasers = parseDOM(container, name=list_type, attrs={'class': list_class}, ret=False) - items = parseDOM(teasers, name='article', attrs={'class': "b-teaser"}, ret=False) - - for item in items: - subtitle = parseDOM(item, name='h4', attrs={'class': "profile"}, ret=False) - subtitle = replaceHTMLCodes(subtitle[0]) - - title = parseDOM(item, name='h5', attrs={'class': "teaser-title.*?"}, ret=False) - title = replaceHTMLCodes(title[0]) - - desc = parseDOM(item, name='p', attrs={'class': "description.*?"}, ret=False) - if len(desc): - desc = replaceHTMLCodes(desc[0]) - else: - desc = "" - - channel = parseDOM(item, name='p', attrs={'class': "channel"}, ret=False) - if len(channel): - channel = replaceHTMLCodes(channel[0]) - else: - channel = "" - date = parseDOM(item, name='span', attrs={'class': 'date'}, ret=False) - date = date[0] - - time = parseDOM(item, name='span', attrs={'class': 'time'}, ret=False) - time = time[0] - - figure = parseDOM(item, name='figure', attrs={'class': 'teaser-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='data-src') - image = replaceHTMLCodes(image[0]) - - link = parseDOM(item, name='a', attrs={'class': 'teaser-link.*?'}, ret='href') - link = link[0] - - desc = self.formatDescription(title, channel, subtitle, desc, date, time) - - parameters = {"link": link, "banner": image, "mode": "openSeries"} - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - def getLaneTopicOverview(self, url): - html = fetchPage({'link': url}) - container = parseDOM(html.get("content"), name='section', attrs={'class': "b-list-container"}, ret=False) - - items = parseDOM(container, name='div', attrs={'class': "b-lane.*?"}, ret=False) - - for item in items: - title_link = parseDOM(item, name='h3', attrs={'class': "title"}, ret=False) - - title = parseDOM(title_link, name='a', attrs={}, ret=False) - title = replaceHTMLCodes(title[0]) - - link = parseDOM(title_link, name='a', attrs={}, ret='href') - link = link[0] - link = "%s%s" % (self.__urlBase, link) - - desc = "" - desc = self.formatDescription(title, "", "", desc, "", "") - - figure = parseDOM(item, name='figure', attrs={'class': 'teaser-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='src') - image = replaceHTMLCodes(image[0]) - - parameters = {"link": link, "banner": image, "mode": "getArchiveDetail"} - - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - def formatDescription(self, title, channel, subtitle, desc, date, time): - date_prefix = self.translation(30009) - - # Reformat Title - if subtitle != title: - if len(subtitle): - title = "%s | %s" % (title, subtitle) - if date != "": - title = "%s - %s" % (title, date) - - # Reformat - if len(subtitle): - subtitle = re.sub("\s\s+", " ", str(subtitle)) - if subtitle == title: - subtitle = "" - else: - if len(channel): - subtitle = " | [LIGHT]%s[/LIGHT]" % subtitle - else: - subtitle = "[LIGHT]%s[/LIGHT]" % subtitle - else: - subtitle = "" - - if len(desc): - desc = "[CR]%s" % desc - else: - desc = "" - - if len(channel): - channel = "[B]%s[/B]" % channel - else: - channel = "" - - if len(date): - return "%s%s[CR]%s[CR][I]%s %s - %s[/I]" % (channel, subtitle, desc, date_prefix, date, time) - else: - return "%s%s[CR]%s" % (channel, subtitle, desc) - - # Parses the frontpage teaser slider - def getTeaserSlideshow(self, url): - url = unqoute_url(url) - html = fetchPage({'link': url}) - container = parseDOM(html.get("content"), name='main', attrs={'class': "main"}, ret=False) - teasers = parseDOM(container, name='div', attrs={'class': "stage-item-list.*?"}, ret=False) - items = parseDOM(teasers, name='a', attrs={'class': "stage-item.*?"}, ret=False) - items_href = parseDOM(teasers, name='a', attrs={'class': "stage-item.*?"}, ret='href') - current = 0 - for item in items: - subtitle = parseDOM(item, name='h2', attrs={'class': "stage-item-profile-title"}, ret=False) - subtitle = replaceHTMLCodes(subtitle[0]) - - title = parseDOM(item, name='h3', attrs={'class': "stage-item-teaser-title"}, ret=False) - title = replaceHTMLCodes(title[0]) - - figure = parseDOM(item, name='figure', attrs={'class': 'stage-item-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={'class': "lazyload"}, ret='data-src') - - image = replaceHTMLCodes(image[0]) - - link = items_href[current] - link = link - - # Reformat Title - if subtitle != title: - title = "%s | %s" % (subtitle, title) - - parameters = {"link": link, "banner": image, "mode": "openSeries"} - - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", "", "", "", "", url) - current += 1 - - # Scrapes the detail page for a schedule day selection (missed a show) - def openArchiv(self, url): - url = unqoute_url(url) - html = fetchPage({'link': url}) - container = parseDOM(html.get("content"), name='main', attrs={'class': "main"}, ret=False) - teasers = parseDOM(container, name='div', attrs={'class': "b-schedule-list"}, ret=False) - items = parseDOM(teasers, name='article', attrs={'class': "b-schedule-episode.*?"}, ret=False) - - date = parseDOM(teasers, name='h2', attrs={'class': 'day-title.*?'}, ret=False) - if len(date): - date = date[0] - else: - date = "" - - for item in items: - title = parseDOM(item, name='h4', attrs={'class': "item-title.*?"}, ret=False) - title = replaceHTMLCodes(title[0]) - - desc = parseDOM(item, name='div', attrs={'class': "item-description.*?"}, ret=False) - if len(desc): - desc = replaceHTMLCodes(desc[0]) - desc = stripTags(desc) - else: - desc = "" - - channel = parseDOM(item, name='span', attrs={'class': "small-information.meta.meta-channel-name"}, ret=False) - if len(channel): - channel = replaceHTMLCodes(channel[0]) - else: - channel = "" - - time = parseDOM(item, name='span', attrs={'class': 'meta.meta-time'}, ret=False) - time = time[0] - - title = "[%s] %s" % (time, title) - - subtitle = time - - image = parseDOM(item, name='img', attrs={}, ret='src') - if len(image): - image = replaceHTMLCodes(image[0]) - else: - image = "" - - link = parseDOM(item, name='a', attrs={'class': 'episode-content'}, ret='href') - link = link[0] - - desc = self.formatDescription(title, channel, subtitle, desc, date, time) - - parameters = {"link": link, "banner": image, "mode": "openSeries"} - - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - # Parses the Frontpage Show Overview Carousel - def getCategories(self): - html = fetchPage({'link': self.__urlShows}) - container = parseDOM(html.get("content"), name='main', attrs={'class': "main"}, ret=False) - teasers = parseDOM(container, name='div', attrs={'class': "b-profile-results-container.*?"}, ret=False) - items = parseDOM(teasers, name='article', attrs={'class': "b-teaser"}, ret=False) - - for item in items: - subtitle = parseDOM(item, name='h4', attrs={'class': "profile"}, ret=False) - subtitle = replaceHTMLCodes(subtitle[0]) - - title = parseDOM(item, name='h5', attrs={'class': "teaser-title.*?"}, ret=False) - title = replaceHTMLCodes(title[0]) - - desc = parseDOM(item, name='p', attrs={'class': "description.*?"}, ret=False) - if len(desc): - desc = replaceHTMLCodes(desc[0]) - else: - desc = "" - - channel = parseDOM(item, name='p', attrs={'class': "channel"}, ret=False) - if len(channel): - channel = replaceHTMLCodes(channel[0]) - else: - channel = "" - date = parseDOM(item, name='span', attrs={'class': 'date'}, ret=False) - date = date[0] - - time = parseDOM(item, name='span', attrs={'class': 'time'}, ret=False) - time = time[0] - - figure = parseDOM(item, name='figure', attrs={'class': 'teaser-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='src') - image = replaceHTMLCodes(image[0]) - - link = parseDOM(item, name='a', attrs={'class': 'teaser-link.*?'}, ret='href') - link = link[0] - - try: - regex = r"https://tvthek.orf.at/profile/(.*)/(.*)/(.*)/(.*)" - matches = re.search(regex, link) - name_path = matches.group(1) - id_path = matches.group(2) - link = "%s/%s/%s/%s" % (self.__urlBase, "profile", name_path, id_path) - except IndexError: - debugLog("Not a standard show link. Using default url: %s" % link) - - desc = self.formatDescription(title, channel, subtitle, desc, date, time) - debugLog("Link: %s" % link) - parameters = {"link": link, "banner": image, "mode": "getSendungenDetail"} - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - # Parses Details for the selected Show - def getCategoriesDetail(self, category, banner): - url = unqoute_url(category) - banner = unqoute_url(banner) - html = fetchPage({'link': url}) - container = parseDOM(html.get("content"), name='main', attrs={'class': "main"}, ret=False) - - # Main Episode - main_episode_container = parseDOM(container, name='section', attrs={'class': "b-video-details.*?"}, ret=False) - - title = parseDOM(main_episode_container, name='h2', attrs={'class': "description-title.*?"}, ret=False) - title = replaceHTMLCodes(title[0]) - - subtitle = parseDOM(main_episode_container, name='span', attrs={'class': "js-subheadline"}, ret=False) - if len(subtitle): - subtitle = replaceHTMLCodes(subtitle[0]) - else: - subtitle = "" - - desc = parseDOM(main_episode_container, name='p', attrs={'class': "description-text.*?"}, ret=False) - if len(desc): - desc = replaceHTMLCodes(desc[0]) - else: - desc = "" - - channel = parseDOM(main_episode_container, name='span', attrs={'class': "channel.*?"}, ret="aria-label") - if len(channel): - channel = replaceHTMLCodes(channel[0]) - else: - channel = "" - - date = parseDOM(main_episode_container, name='span', attrs={'class': 'date'}, ret=False) - date = date[0] - - time = parseDOM(main_episode_container, name='span', attrs={'class': 'time'}, ret=False) - time = time[0] - - image = banner - - if date != "": - title = "%s - %s" % (title, date) - - desc = self.formatDescription(title, channel, subtitle, desc, date, time) - - parameters = {"link": url, "banner": image, "mode": "openSeries"} - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - # More Episodes - more_episode_container = parseDOM(container, name='section', attrs={'class': "related-videos"}, ret=False) - more_episode_json = parseDOM(more_episode_container, name="div", attrs={'class': 'more-episodes.*?'}, ret='data-jsb') - if len(more_episode_json): - more_episode_json_raw = replaceHTMLCodes(more_episode_json[0]) - more_episode_json_data = json.loads(more_episode_json_raw) - more_episodes_url = "%s%s" % (self.__urlBase, more_episode_json_data.get('url')) - - additional_html = fetchPage({'link': more_episodes_url}) - - items = parseDOM(additional_html.get("content"), name='article', attrs={'class': "b-teaser"}, ret=False) - - for item in items: - subtitle = parseDOM(item, name='h4', attrs={'class': "profile"}, ret=False) - subtitle = replaceHTMLCodes(subtitle[0]) - - title = parseDOM(item, name='h5', attrs={'class': "teaser-title.*?"}, ret=False) - title = replaceHTMLCodes(title[0]) - - desc = parseDOM(item, name='p', attrs={'class': "description.*?"}, ret=False) - if len(desc): - desc = replaceHTMLCodes(desc[0]) - else: - desc = "" - - channel = parseDOM(item, name='p', attrs={'class': "channel"}, ret=False) - if len(channel): - channel = replaceHTMLCodes(channel[0]) - else: - channel = "" - date = parseDOM(item, name='span', attrs={'class': 'date'}, ret=False) - date = date[0] - - time = parseDOM(item, name='span', attrs={'class': 'time'}, ret=False) - time = time[0] - - figure = parseDOM(item, name='figure', attrs={'class': 'teaser-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='src') - image = replaceHTMLCodes(image[0]) - - link = parseDOM(item, name='a', attrs={'class': 'teaser-link.*?'}, ret='href') - link = link[0] - - if date != "": - title = "%s - %s" % (title, date) - - desc = self.formatDescription(title, channel, subtitle, desc, date, time) - - parameters = {"link": link, "banner": image, "mode": "openSeries"} - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - def getLaneTeasers(self, html): - items = parseDOM(html.get("content"), name='article', attrs={'class': "b-topic-teaser"}, ret=False) - - lane_title = parseDOM(html.get("content"), name='h3', attrs={'class': "title"}, ret=False) - lane_title = replaceHTMLCodes(lane_title[0]) - lane_title = stripTags(lane_title) - - for item in items: - title = parseDOM(item, name='h5', attrs={'class': "teaser-title.*?"}, ret=False) - title = replaceHTMLCodes(title[0]) - title = "[%s] %s" % (lane_title, title) - - video_count = parseDOM(item, name='p', attrs={'class': "topic-video-count"}, ret=False) - desc = replaceHTMLCodes(video_count[0]) - - figure = parseDOM(item, name='figure', attrs={'class': 'teaser-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='src') - image = replaceHTMLCodes(image[0]) - - link = parseDOM(item, name='a', ret='href') - link = link[0] - link = "%s%s" % (self.__urlBase, link) - - desc = self.formatDescription(title, "", "", desc, "", "") - - parameters = {"link": link, "banner": image, "mode": "getArchiveDetail"} - - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - # Parses Teaserblock Titles and returns links for every category - def getLaneItems(self, url): - html = fetchPage({'link': url}) - items = parseDOM(html.get("content"), name='article', attrs={'class': "b-teaser"}, ret=False) - - if len(items) < 1: - self.getLaneTeasers(html) - else: - lane_title = parseDOM(html.get("content"), name='h3', attrs={'class': "title"}, ret=False) - lane_title = replaceHTMLCodes(lane_title[0]) - lane_title = stripTags(lane_title) - for item in items: - subtitle = parseDOM(item, name='h4', attrs={'class': "profile"}, ret=False) - subtitle = replaceHTMLCodes(subtitle[0]) - - title = parseDOM(item, name='h5', attrs={'class': "teaser-title.*?"}, ret=False) - title = replaceHTMLCodes(title[0]) - title = "[%s] %s" % (lane_title, title) - - desc = parseDOM(item, name='p', attrs={'class': "description.*?"}, ret=False) - if len(desc): - desc = replaceHTMLCodes(desc[0]) - else: - desc = "" - - channel = parseDOM(item, name='p', attrs={'class': "channel"}, ret=False) - if len(channel): - channel = replaceHTMLCodes(channel[0]) - else: - channel = "" - date = parseDOM(item, name='span', attrs={'class': 'date'}, ret=False) - date = date[0] - - time = parseDOM(item, name='span', attrs={'class': 'time'}, ret=False) - time = time[0] - - figure = parseDOM(item, name='figure', attrs={'class': 'teaser-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='src') - image = replaceHTMLCodes(image[0]) - - link = parseDOM(item, name='a', attrs={'class': 'teaser-link.*?'}, ret='href') - link = link[0] - - if date != "": - title = "%s - %s" % (title, date) - - desc = self.formatDescription(title, channel, subtitle, desc, date, time) - - parameters = {"link": link, "banner": image, "mode": "openSeries"} - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - # Parses "Sendung verpasst?" Date Listing - def getSchedule(self): - html = fetchPage({'link': self.__urlSchedule}) - container = parseDOM(html.get("content"), name='div', attrs={'class': 'b-select-box.*?'}) - list_container = parseDOM(container, name='select', attrs={'class': 'select-box-list.*?'}) - items = parseDOM(list_container, name='option', attrs={'class': 'select-box-item.*?'}) - data_items = parseDOM(list_container, name='option', attrs={'class': 'select-box-item.*?'}, ret="data-custom-properties") - i = 0 - for item in items: - title = replaceHTMLCodes(item) - link = replaceHTMLCodes(data_items[i]) - link = "%s%s" % (self.__urlBase, link) - - parameters = {"link": link, "mode": "getScheduleDetail"} - url = build_kodi_url(parameters) - self.html2ListItem(title, "", "", "", "", "", "", url) - i += 1 - - def getArchiv(self): - html = fetchPage({'link': self.__urlArchive}) - html_content = html.get("content") - - wrapper = parseDOM(html_content, name='main', attrs={'class': 'main'}) - items = parseDOM(wrapper, name='article', attrs={'class': 'b-topic-teaser.*?'}) - - for item in items: - subtitle = parseDOM(item, name='h4', attrs={'class': "sub-headline"}, ret=False) - subtitle = replaceHTMLCodes(subtitle[0]) - - title = parseDOM(item, name='h5', attrs={'class': "teaser-title.*?"}, ret=False) - title = replaceHTMLCodes(title[0]) - - video_count = parseDOM(item, name='p', attrs={'class': "topic-video-count"}, ret=False) - desc = replaceHTMLCodes(video_count[0]) - - figure = parseDOM(item, name='figure', attrs={'class': 'teaser-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='src') - image = replaceHTMLCodes(image[0]) - - link = parseDOM(item, name='a', ret='href') - link = link[0] - - desc = self.formatDescription(title, "", subtitle, desc, "", "") - - parameters = {"link": link, "banner": image, "mode": "getArchiveDetail"} - - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - # Creates a XBMC List Item - def html2ListItem(self, title, banner, backdrop, description, duration, date, channel, videourl, subtitles=None, folder=True, playable=False, contextMenuItems=None): - if banner == '': - banner = self.defaultbanner - if backdrop == '': - backdrop = self.defaultbackdrop - params = parameters_string_to_dict(videourl) - mode = params.get('mode') - if not mode: - mode = "play" - - blacklist = False - if self.enableBlacklist: - if mode == 'openSeries' or mode == 'getSendungenDetail': - blacklist = True - debugLog("Adding List Item") - debugLog("Mode: %s" % mode) - debugLog("Videourl: %s" % videourl) - debugLog("Duration: %s" % duration) - debugLog("Banner: %s" % banner) - debugLog("Backdrop: %s" % backdrop) - debugLog("Playable: %s" % playable) - - return createListItem(title, banner, description, duration, date, channel, videourl, playable, folder, backdrop, self.pluginhandle, subtitles, blacklist, contextMenuItems) - - def getMainStreamInfos(self, html, data_json, banner): - stream_info = {} - try: - html_data = parseDOM(html.get("content"), name='section', attrs={'class': "b-video-details.*?"}, ret=False) - playlist_json = data_json.get('playlist') - drm_license_url = self.getDRMLicense(data_json) - - current_channel = parseDOM(html_data, name='span', attrs={'class': "channel.*?"}, ret='aria-label') - if len(current_channel): - stream_info['channel'] = replaceHTMLCodes(current_channel[0]) - else: - stream_info['channel'] = "" - - current_date = parseDOM(html_data, name='span', attrs={'class': 'date'}, ret=False) - stream_info['date'] = current_date[0] - - current_time = parseDOM(html_data, name='span', attrs={'class': 'time'}, ret=False) - if len(current_time): - stream_info['time'] = current_time[0] - else: - stream_info['time'] = "" - - stream_info['second_headline'] = "" - current_subtitle = parseDOM(html_data, name='p', attrs={'class': "profile.*?"}, ret=False) - current_subheadline = parseDOM(current_subtitle, name='span', attrs={'class': "js-subheadline"}, ret=False) - if len(current_subheadline): - stream_info['second_headline'] = stripTags(replaceHTMLCodes(current_subheadline[0])) - else: - if len(current_subtitle): - stream_info['second_headline'] = stripTags(replaceHTMLCodes(current_subtitle[0])) - - if len(html_data): - html_desc = parseDOM(html_data, name='p', attrs={'class': "description-text.*?"}, ret=False) - stream_info['description'] = stripTags(replaceHTMLCodes(html_desc[0])) - - stream_info['main_title'] = playlist_json['title'] - if "preview_image_url" in playlist_json: - stream_info['teaser_image'] = playlist_json['preview_image_url'] - else: - stream_info['teaser_image'] = banner - - stream_info['title'] = data_json.get("selected_video")["title"] - stream_info['full_description'] = self.formatDescription(stream_info['title'], stream_info['channel'], stream_info['second_headline'], stream_info['description'], stream_info['date'], stream_info['time']) - - if data_json.get("selected_video")["description"]: - stream_info['description'] = data_json.get("selected_video")["description"] - - if data_json.get("selected_video")["duration"]: - tmp_duration = float(data_json.get("selected_video")["duration"]) - stream_info['duration'] = int(tmp_duration / 1000) - - if "subtitles" in data_json.get("selected_video"): - main_subtitles = [] - for sub in data_json.get("selected_video")["subtitles"]: - main_subtitles.append(sub.get(u'src')) - stream_info['subtitles'] = main_subtitles - else: - stream_info['subtitles'] = None - stream_info['main_videourl'] = self.getVideoUrl(data_json.get("selected_video")["sources"], drm_license_url) - except: - debugLog("Error fetching stream infos from html") - return stream_info - - # Parses a Video Page and extracts the Playlist/Description/... - def getLinks(self, url, banner, playlist): - url = unqoute_url(url) - debugLog("Loading Videos from %s" % url) - if banner is not None: - banner = unqoute_url(banner) - - stream_infos = {} - playlist_json = {} - video_items = [] - html = fetchPage({'link': url}) - data = parseDOM(html.get("content"), name='div', attrs={'class': "jsb_ jsb_VideoPlaylist"}, ret='data-jsb') - - if len(data): - try: - data = data[0] - data = replaceHTMLCodes(data) - data_json = json.loads(data) - playlist_json = data_json.get('playlist') - stream_infos = self.getMainStreamInfos(html, data_json, banner) - video_items = playlist_json["videos"] - except Exception as e: - debugLog("Error Loading Episode from %s" % url) - - # Add the gapless video if available - try: - drm_license_url = self.getDRMLicense(data_json) - if "is_gapless" in playlist_json: - gapless_subtitles = [] - gapless_name = '-- %s --' % self.translation(30059) - if playlist_json['is_gapless']: - gapless_videourl = self.getVideoUrl(playlist_json['gapless_video']['sources'], drm_license_url) - if gapless_videourl: - if "subtitles" in playlist_json['gapless_video']: - for sub in playlist_json['gapless_video']["subtitles"]: - gapless_subtitles.append(sub.get(u'src')) - else: - global_subtitles = None - if "duration_in_seconds" in playlist_json: - gapless_duration = playlist_json["duration_in_seconds"] - else: - gapless_duration = "" - liz = self.html2ListItem(gapless_name, stream_infos['teaser_image'], "", stream_infos['full_description'], gapless_duration, '', '', gapless_videourl, gapless_subtitles, False, True) - except IndexError as e: - debugLog("No gapless video added for %s" % url) - - - # Multiple chapters available - if len(video_items) > 1: - play_all_name = '-- %s --' % self.translation(30060) - debugLog("Found Video Playlist with %d Items" % len(video_items)) - if self.usePlayAllPlaylist: - createPlayAllItem(play_all_name, self.pluginhandle, stream_infos) - for video_item in video_items: - try: - title = video_item["title"] - if video_item["description"]: - desc = video_item["description"] - else: - debugLog("No Video Description for %s" % title) - desc = "" - - if video_item["duration"]: - duration = float(video_item["duration"]) - duration = int(duration / 1000) - else: - duration = 0 - - preview_img = video_item["preview_image_url"] - sources = video_item["sources"] - if "subtitles" in video_item: - debugLog("Found Subtitles for %s" % title) - subtitles = [] - for sub in video_item["subtitles"]: - subtitles.append(sub.get(u'src')) - else: - subtitles = None - videourl = self.getVideoUrl(sources, drm_license_url) - liz = self.html2ListItem(title, preview_img, "", desc, duration, '', '', videourl, subtitles, False, True) - playlist.add(videourl, liz) - except Exception as e: - debugLog("Error on getLinks") - debugLog(str(e), self.xbmc.LOGERROR) - continue - return playlist - else: - debugLog("No Playlist Items found for %s. Setting up single video view." % stream_infos['title']) - liz = self.html2ListItem(stream_infos['title'], stream_infos['teaser_image'], "", stream_infos['full_description'], stream_infos['duration'], '', '', stream_infos['main_videourl'], stream_infos['subtitles'], False, True) - playlist.add(stream_infos['main_videourl'], liz) - return playlist - else: - showDialog((self.translation(30052))) - sys.exit() - - # Returns Livestream Specials - def getLiveSpecials(self, html): - wrapper = parseDOM(html.get("content"), name='main', attrs={'class': 'main'}) - section = parseDOM(wrapper, name='div', attrs={'class': 'b-special-livestreams-container.*?'}) - items = parseDOM(section, name='div', attrs={'class': 'b-intro-teaser.*?'}) - try: - xbmcaddon.Addon('inputstream.adaptive') - except RuntimeError: - self.html2ListItem("[COLOR red][I] -- %s -- [/I][/COLOR]" % self.translation(30067), self.defaultbanner, "", "", "", "", "Info", "addons://user/kodi.inputstream", None, True, False) - - if items: - debugLog("Found %d Livestream Channels" % len(items)) - for item in items: - channel = "Special" - - debugLog("Processing %s Livestream" % channel) - - figure = parseDOM(item, name='div', attrs={'class': 'img-container'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='src') - image = replaceHTMLCodes(image[0]) - - time = parseDOM(item, name='span', attrs={'class': 'time'}, ret=False) - time = replaceHTMLCodes(time[0]) - time = stripTags(time) - - title = parseDOM(item, name='h4', attrs={'class': 'special-livestream-headline.*?'}) - title = replaceHTMLCodes(title[0]) - - desc = parseDOM(item, name='p', attrs={'class': 'description.*?'}, ret=False) - desc = replaceHTMLCodes(desc[0]) - desc = stripTags(desc) - - link = parseDOM(figure, name='a', attrs={}, ret="href") - link = replaceHTMLCodes(link[0]) - - online = parseDOM(item, name='span', attrs={'class': 'status-online'}) - if len(online): - online = True - else: - online = False - - restart = parseDOM(item, name='span', attrs={'class': 'is-restartable'}) - if len(restart): - restart = True - else: - restart = False - self.buildLivestream(title, link, time, restart, channel, image, online, desc) - - # Returns Live Stream Listing - def getLiveStreams(self): - html = fetchPage({'link': self.__urlLive}) - wrapper = parseDOM(html.get("content"), name='div', attrs={'class': 'all-livestream-container'}) - items = parseDOM(wrapper, name='div', attrs={'class': 'b-lane.*?'}) - - try: - xbmcaddon.Addon('inputstream.adaptive') - except RuntimeError: - self.html2ListItem("[COLOR red][I] -- %s -- [/I][/COLOR]" % self.translation(30067), self.defaultbanner, "", "", "", "", "Info", "addons://user/kodi.inputstream", None, True, False) - - if items: - debugLog("Found %d Livestream Channels" % len(items)) - for item in items: - channel = parseDOM(item, name='img', attrs={'class': 'channel-logo'}, ret="alt") - channel = replaceHTMLCodes(channel[0]) - - debugLog("Processing %s Livestream" % channel) - articles = parseDOM(item, name='li', attrs={'class': 'lane-item.*?'}) - article_links = parseDOM(item, name='a', attrs={'class': 'js-link-box'}, ret='href') - for article_index, article in enumerate(articles): - livestream = parseDOM(article, name='article', attrs={'class': 'b-livestream-teaser is-live'}, ret=False) - if livestream: - figure = parseDOM(livestream, name='figure', attrs={'class': 'teaser-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='data-src') - if len(image) > 0: - image = replaceHTMLCodes(image[0]) - else: - image = "" - - time = parseDOM(livestream, name='h4', attrs={'class': 'time'}, ret=False) - time = replaceHTMLCodes(time[0]) - time = stripTags(time) - - title = parseDOM(livestream, name='h4', attrs={'class': 'livestream-title.*?'}) - title = replaceHTMLCodes(title[0]) - title = stripTags(title) - - link = article_links[article_index] - link = replaceHTMLCodes(link) - - restart = parseDOM(article, name='span', attrs={'class': 'is-restartable'}) - if len(restart): - restart = True - else: - restart = False - - if link.strip() != "" and link != "#": - self.buildLivestream(title, link, time, restart, channel, image, True) - self.getLiveBundesland(items) - self.getLiveSpecials(html) - - def getLiveBundesland(self, items): - for item in items: - channel = parseDOM(item, name='img', attrs={'class': 'channel-logo'}, ret="alt") - channel = replaceHTMLCodes(channel[0]) - - debugLog("Processing %s Livestream" % channel) - articles = parseDOM(item, name='li', attrs={'class': 'lane-item.*?', 'data-jsb': '*'}) - articles_data = parseDOM(item, name='li', attrs={'class': 'lane-item.*?'}, ret='data-jsb') - for article_index, article in enumerate(articles): - livestream = parseDOM(article, name='article', attrs={'class': 'b-livestream-teaser is-live'}, ret=False) - if livestream: - data = articles_data[article_index] - bundesland_data = replaceHTMLCodes(data) - bundesland_data = json.loads(bundesland_data) - for bundesland_stream in bundesland_data: - bundesland_title = bundesland_data[bundesland_stream]['title'] - bundesland_link = bundesland_data[bundesland_stream]['url'] - bundesland_image = bundesland_data[bundesland_stream]['img'] - self.buildLivestream(bundesland_title, bundesland_link, "", True, channel, bundesland_image, True) - - - def buildLivestream(self, title, link, time, restart, channel, banner, online, description = ""): - html = fetchPage({'link': link}) - debugLog("Loading Livestream Page %s for Channel %s" % (link, channel)) - container = parseDOM(html.get("content"), name='div', attrs={'class': "player_viewport.*?"}) - if len(container): - data = parseDOM(container[0], name='div', attrs={}, ret="data-jsb") - - if online: - state = (self.translation(30019)) - else: - state = (self.translation(30020)) - - if description: - description = "%s \n\n %s" % (description, state) - else: - description = state - - if time: - time_str = " (%s)" % time - else: - time_str = "" - - try: - xbmcaddon.Addon('inputstream.adaptive') - inputstreamAdaptive = True - except RuntimeError: - inputstreamAdaptive = False - - if channel: - channel = "[%s]" % channel - else: - channel = "LIVE" - - streaming_url = self.getLivestreamUrl(data, self.videoQuality) - # Remove Get Parameters because InputStream Adaptive cant handle it. - streaming_url = re.sub(r"\?[\S]+", '', streaming_url, 0) - drm_lic_url = self.getLivestreamDRM(data) - uhd_streaming_url = self.getLivestreamUrl(data, 'UHD', True) - if uhd_streaming_url: - uhd50_streaming_url = uhd_streaming_url.replace('_uhd_25/', '_uhd_50/') - - final_title = "[%s] %s - %s%s" % (self.translation(30063), channel, title, time_str) - - debugLog("DRM License: %s" % drm_lic_url) - if uhd_streaming_url: - debugLog("Adding UHD Livestream from %s" % uhd_streaming_url) - uhdContextMenuItems = [] - if inputstreamAdaptive and restart and online: - uhd_restart_parameters = {"mode": "liveStreamRestart", "link": link, "lic_url": drm_lic_url} - uhd_restart_url = build_kodi_url(uhd_restart_parameters) - uhdContextMenuItems.append(('Restart', 'RunPlugin(%s)' % uhd_restart_url)) - uhd_final_title = "[%s] %s [UHD] - %s%s" % (self.translation(30063), channel, title, time_str) - uhd50_final_title = "[%s] %s [UHD 50fps] - %s%s" % (self.translation(30063), channel, title, time_str) - else: - uhd_final_title = "%s[UHD] - %s%s" % (channel, title, time_str) - uhd50_final_title = "%s[UHD 50fps] - %s%s" % (channel, title, time_str) - - if not drm_lic_url: - self.html2ListItem(uhd_final_title, banner, "", description, time, channel, channel, generateAddonVideoUrl(uhd_streaming_url), None, False, True, uhdContextMenuItems) - self.html2ListItem(uhd50_final_title, banner, "", description, time, channel, channel, generateAddonVideoUrl(uhd50_streaming_url), None, False, True, uhdContextMenuItems) - elif inputstreamAdaptive: - drm_video_url = generateDRMVideoUrl(uhd_streaming_url, drm_lic_url) - self.html2ListItem(uhd_final_title, banner, "", description, time, channel, channel, drm_video_url, None, False, True, uhdContextMenuItems) - drm50_video_url = generateDRMVideoUrl(uhd50_streaming_url, drm_lic_url) - self.html2ListItem(uhd50_final_title, banner, "", description, time, channel, channel, drm50_video_url, None, False, True, uhdContextMenuItems) - - if streaming_url: - contextMenuItems = [] - if inputstreamAdaptive and restart and online: - debugLog("Adding DRM Restart %s" % drm_lic_url) - restart_parameters = {"mode": "liveStreamRestart", "link": link, "lic_url": drm_lic_url} - restart_url = build_kodi_url(restart_parameters) - contextMenuItems.append((self.translation(30063), 'RunPlugin(%s)' % restart_url)) - - else: - final_title = "%s - %s%s" % (channel, title, time_str) - - if not drm_lic_url: - self.html2ListItem(final_title, banner, "", description, time, channel, channel, generateAddonVideoUrl(streaming_url), None, False, True, contextMenuItems) - elif inputstreamAdaptive: - drm_video_url = generateDRMVideoUrl(streaming_url, drm_lic_url) - self.html2ListItem(final_title, banner, "", description, time, channel, channel, drm_video_url, None, False, - True, contextMenuItems) - - def getDRMLicense(self, data): - try: - if 'drm' in data and 'widevineUrl' in data['drm']: - debugLog("Widevine Url found %s" % data['drm']['widevineUrl']) - widevineUrl = data['drm']['widevineUrl'] - token = data['drm']['token'] - brand = data['drm']['brandGuid'] - return "%s?BrandGuid=%s&userToken=%s" % (widevineUrl, brand, token) - except: - debugLog("No License Url found") - - def getLivestreamDRM(self, data_sets): - for data in data_sets: - try: - data = replaceHTMLCodes(data) - data = json.loads(data) - drm_lic = self.getDRMLicense(data) - if drm_lic: - return drm_lic - except Exception as e: - debugLog("Error getting Livestream DRM Keys") - - def liveStreamRestart(self, link, protocol): - try: - xbmcaddon.Addon('inputstream.adaptive') - except RuntimeError: - return - - html = fetchPage({'link': link}) - bitmovinStreamId = self.getLivestreamBitmovinID(html) - stream_info = self.getLivestreamInformation(html) - - if bitmovinStreamId: - title = stream_info['title'] - image = stream_info['image'] - description = stream_info['description'] - duration = stream_info['duration'] - date = stream_info['date'] - channel = stream_info['channel'] - - ApiKey = '2e9f11608ede41f1826488f1e23c4a8d' - response = url_get_request('https://playerapi-restarttv.ors.at/livestreams/%s/sections/?state=active&X-Api-Key=%s' % (bitmovinStreamId, ApiKey)) - try: - charset = response.headers.get_content_charset() - response_raw = response.read().decode(charset) - except AttributeError: - response_raw = response.read().decode('utf-8') - - section = json.loads(response_raw) - if len(section): - section = section[0] - streamingURL = 'https://playerapi-restarttv.ors.at/livestreams/%s/sections/%s/manifests/%s/?startTime=%s&X-Api-Key=%s' % (bitmovinStreamId, section.get('id'), protocol, section.get('metaData').get('timestamp'), ApiKey) - - listItem = createListItem(title, image, description, duration, date, channel, streamingURL, True, False, self.defaultbackdrop, self.pluginhandle) - return streamingURL, listItem - - def getLivestreamUrl(self, data_sets, preferred_quality, strict=False): - fallback = {} - for data in data_sets: - try: - data = replaceHTMLCodes(data) - data = json.loads(data) - if 'playlist' in data: - if 'videos' in data['playlist']: - for video_items in data['playlist']['videos']: - for video_sources in video_items['sources']: - - if video_sources['quality'].lower() == preferred_quality.lower() and video_sources[ - 'protocol'].lower() == "http" and video_sources['delivery'].lower() == 'hls': - return video_sources['src'] - elif video_sources['quality'].lower()[0:3] == preferred_quality.lower() and video_sources[ - 'protocol'].lower() == "http" and video_sources['delivery'].lower() == 'dash': - return video_sources['src'] - elif video_sources['quality'] and video_sources['src'] and video_sources['quality'][0:3] in self.__videoQualities: - debugLog("Adding Video Url %s (%s)" % (video_sources['src'], video_sources['delivery'])) - fallback[video_sources['quality'].lower()[0:3]] = video_sources['src'] - if not strict: - for quality in reversed(self.__videoQualities): - debugLog("Looking for Fallback Quality %s" % quality) - if quality.lower() in fallback: - debugLog("Returning Fallback Stream %s" % quality) - return fallback[quality.lower()] - except Exception as e: - debugLog("Error getting Livestream") - - @staticmethod - def getLivestreamJSON(html, key_check='restart_url'): - container = parseDOM(html.get("content"), name='div', attrs={'class': "player_viewport.*?"}) - if len(container): - data_sets = parseDOM(container[0], name='div', attrs={}, ret="data-jsb") - if len(data_sets): - for data in data_sets: - try: - data = replaceHTMLCodes(data) - data = json.loads(data) - if key_check in data: - return data - except Exception as e: - debugLog("Error getting Livestream JSON for key %s" % key_check) - return False - - def getLivestreamBitmovinID(self, html): - data = self.getLivestreamJSON(html, 'restart_url') - if data: - try: - bitmovin_id = data['restart_url'].replace("https://playerapi-restarttv.ors.at/livestreams/", "").replace("/sections/", "") - return bitmovin_id.split("?")[0] - except Exception as e: - debugLog("Error getting Livestream Bitmovin ID") - - def getLivestreamLicenseData(self, html): - data = self.getLivestreamJSON(html, 'drm') - if data: - try: - return self.getLivestreamDRM(data) - except Exception as e: - debugLog("Error getting Livestream DRM License") - - @staticmethod - def getLivestreamInformation(html): - container = parseDOM(html.get("content"), name='div', attrs={'class': "player_viewport.*?"}) - data_sets = parseDOM(container[0], name='div', attrs={}, ret="data-jsb") - title = "Titel" - image = "" - description = "Beschreibung" - duration = "" - date = "" - channel = "" - - for data in data_sets: - try: - data = replaceHTMLCodes(data) - data = json.loads(data) - - if 'playlist' in data: - time_str = False - time_str_end = False - if 'title' in data['playlist']: - title = data['playlist']['title'] - if 'preview_image_url' in data['playlist']: - image = data['playlist']['preview_image_url'] - if 'livestream_start' in data['playlist']: - date = data['playlist']['livestream_start'] - time_str = datetime.datetime.fromtimestamp(int(date)).strftime('%H:%M') - if 'livestream_end' in data['playlist']: - date = data['playlist']['livestream_end'] - time_str_end = datetime.datetime.fromtimestamp(int(date)).strftime('%H:%M') - if 'videos' in data['playlist']: - if 'description' in data['playlist']['videos']: - description = data['playlist']['videos']['description'] - if time_str and time_str_end: - return {"title": "%s (%s - %s)" % (title, time_str, time_str_end), "image": image, "description": description, "date": date, "duration": duration, "channel": channel} - else: - return {"title": title, "image": image, "description": description, "date": date, "duration": duration, "channel": channel} - except Exception as e: - debugLog("Error getting Livestream Infos") - - # Parses the Topic Overview Page - def getThemen(self): - html = fetchPage({'link': self.__urlTopics}) - html_content = html.get("content") - - content = parseDOM(html_content, name='section', attrs={}) - - for topic in content: - title = parseDOM(topic, name='h3', attrs={'class': 'item_wrapper_headline.subheadline'}) - if title: - title = replaceHTMLCodes(title[0]) - - link = parseDOM(topic, name='a', attrs={'class': 'more.service_link.service_link_more'}, ret="href") - link = replaceHTMLCodes(link[0]) - - image = parseDOM(topic, name='img', ret="src") - image = replaceHTMLCodes(image[0]).replace("width=395", "width=500").replace("height=209.07070707071", "height=265") - - descs = parseDOM(topic, name='h4', attrs={'class': 'item_title'}) - description = "" - for desc in descs: - description += "* %s \n" % replaceHTMLCodes(desc) - - parameters = {"link": link, "mode": "getThemenDetail"} - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", description, "", "", "", url) - - # Parses the Archive Detail Page - def getArchiveDetail(self, url): - url = unqoute_url(url) - html = fetchPage({'link': url}) - html_content = html.get("content") - - wrapper = parseDOM(html_content, name='main', attrs={'class': 'main'}) - items = parseDOM(wrapper, name='article', attrs={'class': 'b-teaser.*?'}) - - for item in items: - subtitle = parseDOM(item, name='h4', attrs={'class': "profile"}, ret=False) - subtitle = replaceHTMLCodes(subtitle[0]) - - title = parseDOM(item, name='h5', attrs={'class': "teaser-title.*?"}, ret=False) - title = replaceHTMLCodes(title[0]) - - desc = parseDOM(item, name='p', attrs={'class': "description.*?"}, ret=False) - desc = replaceHTMLCodes(desc[0]) - - figure = parseDOM(item, name='figure', attrs={'class': 'teaser-img'}, ret=False) - image = parseDOM(figure, name='img', attrs={}, ret='src') - image = replaceHTMLCodes(image[0]) - - link = parseDOM(item, name='a', ret='href') - link = link[0] - - channel = parseDOM(item, name='p', attrs={'class': "channel"}, ret=False) - if len(channel): - channel = replaceHTMLCodes(channel[0]) - else: - channel = "" - - date = parseDOM(item, name='span', attrs={'class': 'date'}, ret=False) - date = date[0] - - time = parseDOM(item, name='span', attrs={'class': 'time'}, ret=False) - time = time[0] - - desc = self.formatDescription(title, channel, subtitle, desc, date, time) - - parameters = {"link": link, "banner": image, "mode": "openSeries"} - url = build_kodi_url(parameters) - self.html2ListItem(title, image, "", desc, "", "", "", url) - - def getSearchHistory(self): - parameters = {'mode': 'getSearchResults'} - u = build_kodi_url(parameters) - createListItem((self.translation(30007)) + " ...", self.defaultbanner, "", "", "", '', u, False, True, self.defaultbackdrop, self.pluginhandle) - - history = searchHistoryGet() - for str_val in reversed(history): - if str_val.strip() != '': - parameters = {'mode': 'getSearchResults', 'link': str_val.replace(" ", "+")} - u = build_kodi_url(parameters) - createListItem(str_val, self.defaultbanner, "", "", "", '', u, False, True, self.defaultbackdrop, self.pluginhandle) - - @staticmethod - def removeUmlauts(str_val): - return str_val.replace("Ö", "O").replace("ö", "o").replace("Ü", "U").replace("ü", "u").replace("Ä", "A").replace("ä","a") - - def getSearchResults(self, link): - keyboard = self.xbmc.Keyboard(link) - keyboard.doModal() - if keyboard.isConfirmed(): - keyboard_in = keyboard.getText() - if keyboard_in != link: - searchHistoryPush(keyboard_in) - searchurl = "%s?q=%s" % (self.__urlSearch, keyboard_in.replace(" ", "+")) - self.getTeaserList(searchurl, 'b-search-results', 'section') - else: - parameters = {'mode': 'getSearchHistory'} - u = build_kodi_url(parameters) - createListItem((self.translation(30014)) + " ...", self.defaultbanner, "", "", "", '', u, False, True, self.defaultbackdrop, self.pluginhandle) diff --git a/resources/lib/Kodi.py b/resources/lib/Kodi.py new file mode 100644 index 0000000..df1c694 --- /dev/null +++ b/resources/lib/Kodi.py @@ -0,0 +1,423 @@ +import xbmcaddon +from xbmc import PlayList, PLAYLIST_VIDEO, Player, Keyboard, executebuiltin, log, LOGDEBUG +from xbmcgui import ListItem, Dialog, DialogProgress +from xbmcaddon import Addon +from xbmcplugin import addDirectoryItem, endOfDirectory, setContent, setResolvedUrl, addSortMethod, SORT_METHOD_VIDEO_TITLE, SORT_METHOD_DATE +import xbmcvfs +import sys +import os +import time +import inputstreamhelper +from urllib.parse import unquote + +try: + from OrfOn import * +except ModuleNotFoundError: + from resources.lib.OrfOn import * + + +class Kodi: + version_regex = r"plugin:\/\/([^\/]+)" + addon_id = re.search(version_regex, sys.argv[0]).groups()[0] + addon = Addon() + data_folder = xbmcvfs.translatePath("special://profile/addon_data/%s" % addon_id) + + input_stream_protocol = 'mpd' + input_stream_drm_version = 'com.widevine.alpha' + input_stream_mime = 'application/dash+xml' + input_stream_license_contenttype = 'application/octet-stream' + + geo_lock = False + max_cache_age = 60 * 60 * 24 + + def __init__(self, plugin): + self.plugin = plugin + self.init_storage() + self.base_path = self.addon.getAddonInfo('path') + self.resource_path = os.path.join(self.base_path, "resources") + self.use_subtitles = self.addon.getSetting('useSubtitles') == 'true' + self.use_segments = self.addon.getSetting('useSegments') == 'true' + self.show_segments = self.addon.getSetting('showSegments') == 'true' + self.use_timeshift = self.addon.getSetting('useTimeshift') == 'true' + self.hide_audio_description_content = self.addon.getSetting('hideAD') == 'true' + self.hide_sign_language_content = self.addon.getSetting('hideOEGS') == 'true' + self.useragent = self.addon.getSetting('userAgent') + self.pager_limit = int(self.addon.getSetting('pagerLimit')) + self.max_cache_age = int(self.addon.getSetting('maxCacheAge')) * 60 * 60 * 24 + + def init_storage(self): + if not os.path.exists(self.data_folder): + os.makedirs(self.data_folder) + + def translate(self, translation_id): + translation = self.addon.getLocalizedString + return translation(translation_id) + + def get_translation(self, translation_id, fallback, replace=None): + translation = self.translate(translation_id) + if translation: + if replace is not None: + return replace % translation + else: + return translation + return fallback + + def is_geo_locked(self) -> bool: + return self.geo_lock + + def hide_audio_description(self) -> bool: + return self.hide_audio_description_content + + def hide_sign_language(self) -> bool: + return self.hide_sign_language_content + + def hide_accessibility_menu(self) -> bool: + return self.hide_sign_language_content and self.hide_audio_description_content and not self.use_subtitles + + def set_geo_lock(self, lock): + self.geo_lock = lock + + def hide_content(self, item) -> bool: + if self.hide_audio_description_content and item.has_audio_description(): + self.log("Hiding %s because AD content hide is enabled in settings" % item.label()) + return True + if self.hide_sign_language_content and item.has_sign_language(): + self.log("Hiding %s because OEGS content hide is enabled in settings" % item.label()) + return True + if self.geo_lock and item.is_geo_locked(): + self.log("Hiding %s because GEO Lock is active for your ISP" % item.label()) + return True + return False + + def render(self, item): + if not self.hide_content(item): + if item.is_playable(): + list_item = self.render_video(item) + link = item.url() + route = self.plugin.url_for_path(link) + if self.use_segments and self.show_segments and item.has_segments(): + folder = True + else: + folder = False + addDirectoryItem(self.plugin.handle, url=route, listitem=list_item, isFolder=folder) + else: + list_item = self.render_directory(item) + link = item.url() + route = self.plugin.url_for_path(link) + addDirectoryItem(self.plugin.handle, url=route, listitem=list_item, isFolder=True) + + def restart(self, video): + self.log("Running Restart Play Action") + play_item = self.render_video(video) + play_item.setProperty('inputstream.adaptive.play_timeshift_buffer', 'true') + streaming_url = video.get_stream().get('url') + Player().play(streaming_url, play_item) + + def build_stream_url(self, url): + return "%s|User-Agent=%s" % (url, self.useragent) + + def play_url(self, url): + url = self.build_stream_url(unquote(url)) + play_item = ListItem(path=url, offscreen=True) + setResolvedUrl(self.plugin.handle, True, play_item) + + def play(self, videos): + if len(videos) < 1: + Dialog().notification('No Stream available', 'Unable to find a stream for this content', xbmcaddon.Addon().getAddonInfo('icon')) + self.log("Running Play Action") + playlist = PlayList(PLAYLIST_VIDEO) + tracks = [] + for video in videos: + tracks.append(video) + + if len(tracks) > 1: + if self.show_segments: + for track in tracks: + self.render(track) + else: + for track in tracks: + play_item = self.render_video(track) + play_stream = self.build_stream_url(unquote(track.get_stream().get('url'))) + playlist.add(play_stream, play_item) + self.log("Playing Playlist %s from position %d" % (playlist.size(), playlist.getposition())) + else: + self.log("Playing Single Video") + for track in tracks: + play_item = self.render_video(track) + setResolvedUrl(self.plugin.handle, True, play_item) + break + + def render_directory(self, directory) -> ListItem: + title = directory.label() + title2 = directory.label2() + + list_item = ListItem(offscreen=True) + list_item.setContentLookup(False) + list_item.setLabel(title) + list_item.setLabel2(title2) + item_info = self.build_info(directory) + list_item.setInfo(type="Video", infoLabels=item_info) + list_item.setIsFolder(not directory.is_playable()) + list_item.setProperty("IsPlayable", str(directory.is_playable())) + item_art = self.build_art(directory) + list_item.setArt(item_art) + return list_item + + def render_video(self, teaser) -> ListItem: + title = teaser.label() + title2 = teaser.label2() + stream_url = self.build_stream_url(unquote(teaser.url())) + + headers = "User-Agent=%s&Content-Type=%s" % (self.useragent, self.input_stream_license_contenttype) + is_helper = inputstreamhelper.Helper(self.input_stream_protocol, drm=self.input_stream_drm_version) + if is_helper.check_inputstream(): + list_item = ListItem(path=stream_url, offscreen=True) + list_item.setContentLookup(False) + + if teaser.get_stream(): + self.log("Found Stream for Video %s" % teaser.label()) + self.log("Stream: (%s)" % teaser.url()) + stream_data = teaser.get_stream() + list_item.setProperty('inputstream', 'inputstream.adaptive') + list_item.setProperty('inputstream.adaptive.stream_headers', headers) + list_item.setProperty('inputstream.adaptive.manifest_type', self.input_stream_protocol) + + if self.use_subtitles and stream_data.get('subtitle') and stream_data.get('subtitle') is not None: + list_item.setSubtitles([stream_data.get('subtitle')]) + list_item.addStreamInfo('subtitle', {'language': 'deu'}) + + if stream_data['drm']: + self.log("Video %s is DRM protected. Adding DRM relevant parameters" % teaser.label()) + list_item.setMimeType(self.input_stream_mime) + list_item.setProperty('inputstream', 'inputstream.adaptive') + list_item.setProperty('inputstream.adaptive.stream_headers', headers) + list_item.setProperty('inputstream', is_helper.inputstream_addon) + list_item.setProperty('inputstream.adaptive.manifest_type', self.input_stream_protocol) + license_url = "%s?BrandGuid=%s&userToken=%s" % (stream_data.get('drm_widewine_url'), stream_data.get('drm_widewine_brand'), stream_data.get('drm_token')) + list_item.setProperty('inputstream.adaptive.license_type', self.input_stream_drm_version) + list_item.setProperty('inputstream.adaptive.license_key', license_url + '|' + headers + '|R{SSM}|') + else: + self.log("No Stream for Video %s (%s)" % (teaser.label(), teaser.url()), 'error') + + list_item.setLabel(title) + list_item.setLabel2(title2) + item_info = self.build_info(teaser) + + list_item.setInfo(type="Video", infoLabels=item_info) + list_item.setIsFolder(not teaser.is_playable()) + list_item.setProperty("IsPlayable", str(teaser.is_playable())) + video_w, video_h = teaser.get_resolution() + list_item.addStreamInfo('video', {'aspect': '1.78', 'codec': 'h264', 'width': video_w, 'height': video_h, 'duration': teaser.get_duration()}) + list_item.addStreamInfo('audio', {'codec': 'aac', 'language': 'deu', 'channels': 2}) + + item_art = self.build_art(teaser) + list_item.setArt(item_art) + + context_menu = [] + context_menu_items = teaser.get_context_menu() + for context_menu_item in context_menu_items: + context_menu.append(self.build_context_menu(context_menu_item)) + list_item.addContextMenuItems(context_menu, replaceItems=True) + return list_item + elif not teaser.get_stream(): + Dialog().notification('No Stream available', 'Unable to find a stream for %s' % title, xbmcaddon.Addon().getAddonInfo('icon')) + elif not is_helper.check_inputstream(): + Dialog().notification('Inputstream Adaptive not available', 'Install Inputstream Adaptive and Inputstream Helper', xbmcaddon.Addon().getAddonInfo('icon')) + + def build_info(self, item) -> dict: + desc_prefix = self.build_meta_description(item) + if desc_prefix is not None and item.get_description(): + generated_description = desc_prefix + item.get_description() + generated_outline = desc_prefix + self.truncate_string(item.get_description()) + else: + generated_description = item.get_description() + generated_outline = self.truncate_string(item.get_description()) + return { + 'title': item.label(), + 'originaltitle': item.label(), + 'sorttitle': item.label(), + 'tvshowtitle': item.label(), + 'plot': generated_description, + 'plotoutline': generated_outline, + 'genre': item.genre(), + 'tag': item.genre(), + 'aired': item.date(), + 'country': item.country(), + 'year': item.year(), + 'mediatype': item.media_type(), + 'cast': item.get_cast() + } + + def build_art(self, item) -> dict: + return { + 'thumb': item.thumbnail or self.get_media('icon.jpg'), + 'poster': item.poster or self.get_media('poster.jpg'), + 'fanart': item.backdrop or self.get_media('fanart.jpg'), + } + + def build_context_menu(self, item): + route = self.plugin.url_for_path(item.get('url')) + if item.get('type') == 'run': + return item.get('title'), 'RunPlugin(%s)' % route + else: + return item.get('title'), 'Container.Update(%s)' % route + + def list_callback(self, content_type="movies", sort=False) -> None: + if content_type: + setContent(self.plugin.handle, content_type) + if sort: + addSortMethod(int(sys.argv[1]), SORT_METHOD_DATE) + addSortMethod(int(sys.argv[1]), SORT_METHOD_VIDEO_TITLE) + endOfDirectory(self.plugin.handle, True) + + def get_media(self, filename): + return os.path.join(self.resource_path, filename) + + def clear_stored_directories(self, storage_key): + target_file = '%s.json' % storage_key + self.remove_file(target_file) + + def get_stored_directories(self, storage_key): + target_file = '%s.json' % storage_key + self.init_storage() + json_data = self.load_json(target_file) + directories = [] + for json_item in json_data: + directory = Directory(json_item.get('title'), json_item.get('description'), json_item.get('link'), translator=self) + directories.append(directory) + return directories + + def store_directory(self, directory, storage_key): + target_file = '%s.json' % storage_key + self.init_storage() + + json_data = self.load_json(target_file) + directory_json = { + 'title': directory.title, + 'description': directory.description, + 'link': directory.url() + } + + if json_data: + json_data.append(directory_json) + else: + json_data = [directory_json] + self.save_json(json_data, target_file) + + def remove_file(self, file) -> bool: + file = "%s/%s" % (self.data_folder, file) + try: + os.remove(file) + return True + except FileNotFoundError: + self.log("File %s could not be found. Skipping remove action." % file, 'warning') + return False + + def get_cached_file(self, file) -> tuple: + channel_map_age = self.get_file_age(file) + + if self.max_cache_age > channel_map_age >= 0: + self.log("Channel Cache is valid. File age lower than %s seconds (%d seconds)" % (self.max_cache_age, channel_map_age)) + data = self.load_json(file) + if not len(data): + cached = False + else: + cached = True + else: + self.log("Channel Cache is invalid. Reloading because file age larger than %s seconds (%d seconds)" % (self.max_cache_age, channel_map_age)) + cached = False + self.remove_file(file) + data = {} + return data, cached + + def get_file_age(self, file) -> int: + file = "%s/%s" % (self.data_folder, file) + try: + st = os.stat(file) + age_seconds = int(time.time() - st.st_mtime) + self.log("Cache Age %d seconds" % age_seconds) + return age_seconds + except FileNotFoundError: + self.log("File %s could not be found" % file, 'warning') + return -1 + + def save_json(self, json_data, file) -> bool: + file = "%s/%s" % (self.data_folder, file) + try: + with open(file, 'w') as data_file: + data_file.write(json.dumps(json_data)) + data_file.close() + return True + except TypeError: + self.log("Json file format for %s was invalid. Removing file ..." % file, 'warning') + os.remove(file) + return False + except PermissionError: + self.log("Permission to File %s was denied" % file, 'warning') + return False + + def load_json(self, file) -> list: + file = "%s/%s" % (self.data_folder, file) + self.log("Loading JSON from %s" % file) + try: + with open(file, 'r') as data_file: + data = json.load(data_file) + return data + except FileNotFoundError: + self.log("File %s could not be found" % file, 'warning') + return [] + + @staticmethod + def log(msg, msg_type='info'): + log("[%s][ORFON][KODI] %s" % (msg_type.upper(), msg), LOGDEBUG) + + @staticmethod + def execute(command): + executebuiltin(command) + + @staticmethod + def select_dialog(title, items): + select_dialog = Dialog() + selected = select_dialog.select(title, items) + if selected != -1: + return selected + return False + + @staticmethod + def get_progress_dialog(title, description=""): + progress = DialogProgress() + progress.create(title, description) + return progress + + @staticmethod + def build_meta_description(item): + desc = "" + meta_desc = item.get_meta_description() + for label in meta_desc: + desc += "\n[COLOR blue][LIGHT]%s[/LIGHT][/COLOR] %s" % (label, meta_desc[label]) + if desc != "": + desc += "\n\n" + return desc + + @staticmethod + def truncate_string(str_value, max_len=400) -> str: + if str_value: + return str_value[:max_len] + (str_value[max_len:] and ' ...') + + @staticmethod + def build_url(url, args) -> str: + arg_str = "" + for arg in args: + if not arg_str: + arg_str = "?%s=%s" % (arg, args.get(arg)[0]) + else: + arg_str += "&%s=%s" % (arg, args.get(arg)[0]) + return "%s%s" % (url, arg_str) + + @staticmethod + def get_keyboard_input() -> str: + keyboard = Keyboard() + keyboard.doModal() + if keyboard.isConfirmed(): + return keyboard.getText() + return "" diff --git a/resources/lib/OrfOn.py b/resources/lib/OrfOn.py new file mode 100644 index 0000000..88c1551 --- /dev/null +++ b/resources/lib/OrfOn.py @@ -0,0 +1,689 @@ +import json +import re +from datetime import date, timedelta +try: + from Directory import * +except ModuleNotFoundError: + from resources.lib.Directory import * + +from urllib.request import Request as urllib_Request +from urllib.request import urlopen as urllib_urlopen +from urllib.error import HTTPError as urllib_HTTPError +from urllib.error import URLError as urllib_URLError +from urllib.parse import urlparse, urlencode, quote_plus + + +class OrfOn: + useragent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36' + api_auth = 'Basic b3JmX29uX3Y0MzpqRlJzYk5QRmlQU3h1d25MYllEZkNMVU41WU5aMjhtdA==' + api_version = '4.3' + api_pager_limit = 50 + geo_lock_url = 'https://apasfiis.sf.apa.at/admin/proxycheck/' + api_base = 'https://api-tvthek.orf.at/api/v%s' % api_version + + api_endpoint_settings = '/settings' + api_endpoint_home = '/page/start' + api_endpoint_recently_added = '/page/startpage/newest' + api_endpoint_schedule = '/schedule/%s' + api_endpoint_shows = '/profiles?limit=%d' + api_endpoint_shows_letter = '/profiles/lettergroup/%s' + api_endpoint_history = '/history' + api_endpoint_search = '/search/%s' + api_endpoint_search_partial = '/search-partial/%s/%s?limit=%d' + api_endpoint_livestreams = '/livestreams' + api_endpoint_livestream = '/livestream/%s' + api_endpoint_timeshift = '/timeshift/channel/%d/sources' + api_endpoint_channels = '/channels?limit=200' + api_endpoint_channel_livestream = '/livestreams/channel/%s' + + channel_map = False + settings = False + + use_segments = True + + supported_delivery = 'dash' + quality_definitions = { + 'UHD': { + 'name': 'UHD', + 'width': 3840, + 'height': 2160, + }, + 'QXB': { + 'name': 'Adaptive', + 'width': 1280, + 'height': 720, + }, + 'QXA': { + 'name': 'Adaptive', + 'width': 1280, + 'height': 720, + } + } + drm_widewine_brand = '13f2e056-53fe-4469-ba6d-999970dbe549' + drm_widewine_brand_ts = '319f2ca9-0d0c-4e5b-bb70-72efae61dad7' + + def __init__(self, channel_map=None, settings=None, useragent=False, kodi_worker=None): + self.kodi_worker = kodi_worker + if useragent: + self.useragent = useragent + + self.log("Loading ORF On API") + if not channel_map: + self.channel_map = self.get_channel_map() + else: + self.channel_map = channel_map + + if not settings: + self.settings = self.get_settings() + else: + self.settings = settings + + self.type_map = { + 'highlights': self.translate_string(30115, 'Highlights'), + 'genres': self.translate_string(30116, 'Categories'), + 'orflive': self.translate_string(30113, 'Livestream') + } + + def is_geo_locked(self): + headers = self.get_headers() + url = self.geo_lock_url + try: + self.log("Loading %s" % url) + request = urllib_urlopen(urllib_Request(url, headers=headers)) + except urllib_HTTPError as error: + self.log('%s (%s)' % (error, url), 'error') + return False + except urllib_URLError as error: + self.log('%s (%s)' % (error, url), 'error') + return False + + try: + xml_data = request.read() + pattern = r'isallowed="(\w+)"' + match = re.search(pattern, xml_data.decode('utf-8')) + if match: + is_allowed = match.group(1) + return is_allowed.lower() != 'true' + except re.error as error: + self.log('%s (%s)' % (error, url), 'error') + return False + + def translate_string(self, translation_id, fallback, replace=None): + if self.kodi_worker: + return self.kodi_worker.get_translation(translation_id, fallback, replace) + else: + return fallback + + def set_pager_limit(self, limit): + self.api_pager_limit = limit + + def set_segments_behaviour(self, use_segments): + self.use_segments = use_segments + + def get_auth_headers(self) -> dict: + headers = self.get_headers() + headers.update({'Authorization': self.api_auth}) + return headers + + def get_headers(self) -> dict: + headers = {} + headers.update({'User-Agent': self.useragent}) + return headers + + def get_widevine_url(self) -> str: + return self.settings.get('drm_endpoints').get('widevine') + + def get_widevine_brand(self, timeshift=False) -> str: + if timeshift: + return self.drm_widewine_brand_ts + return self.drm_widewine_brand + + def get_replay_days(self) -> int: + return int(self.settings.get('max_viewing_time')) + + def auth_request(self, url): + headers = self.get_auth_headers() + try: + url = self.api_base + url + self.log("Loading %s" % url) + request = urllib_urlopen(urllib_Request(url, headers=headers)) + except urllib_HTTPError as error: + self.log('%s (%s)' % (error, url), 'error') + return {} + data = request.read() + return json.loads(data) + + def get_main_menu(self) -> list: + items = [Directory(self.translate_string(30144, 'Recently added'), '', '/recent', '', 'new', translator=self.kodi_worker), + Directory(self.translate_string(30110, 'Frontpage'), '', self.api_endpoint_home, '', 'home', translator=self.kodi_worker), + Directory(self.translate_string(30111, 'Schedule'), '', '/schedule', '', 'schedule', translator=self.kodi_worker), + Directory(self.translate_string(30112, 'Shows'), '', self.api_endpoint_shows % self.api_pager_limit, '', 'shows', translator=self.kodi_worker), + Directory(self.translate_string(30113, 'Livestream'), '', self.api_endpoint_livestreams, '', 'live', translator=self.kodi_worker), + Directory(self.translate_string(30114, 'Search'), '', '/search', '', 'search', translator=self.kodi_worker)] + items += self.get_frontpage(lanes=False) + return items + + def get_sign_language_menu(self): + return Directory(self.translate_string(30145, 'Broadcasts using sign language'), '', '/episodes/sign-language', '', 'oegscontent', translator=self.kodi_worker) + + def get_audio_description_menu(self): + return Directory(self.translate_string(30146, 'Broadcasts with audio description'), '', '/episodes/visually-impaired', '', 'adcontent', translator=self.kodi_worker) + + def get_subtitles_menu(self): + return Directory(self.translate_string(30148, 'Broadcasts with subtitles'), '', '/episodes/subtitles', '', 'adcontent', translator=self.kodi_worker) + + def get_settings(self) -> dict: + # Return cached settings + if self.settings: + self.log("Found cached settings") + return self.settings + + self.log("Fetching fresh settings") + url = self.api_endpoint_settings + data = self.auth_request(url) + return data + + def get_channel_map(self) -> dict: + # Return cached channel map + if self.channel_map: + self.log("Found cached channel map") + return self.channel_map + + self.log("Fetching new channel map") + url = self.api_endpoint_channels + data = self.auth_request(url) + channel_map = {} + for channel in data['_embedded']['items']: + name = channel['name'] + channel_id = channel['id'] + reel = channel['reel'] + if 'color_logo' in channel['_links']: + media_url = self.clean_url(channel['_links']['color_logo']['href']) + logo_data = self.auth_request(media_url) + color_logo = logo_data['public_urls']['tiny']['url'] + else: + color_logo = "" + if 'black_and_white_logo' in channel['_links']: + media_url = self.clean_url(channel['_links']['black_and_white_logo']['href']) + logo_data = self.auth_request(media_url) + logo = logo_data['public_urls']['tiny']['url'] + else: + logo = "" + channel_map[channel_id] = { + 'name': name, + 'color_logo': color_logo, + 'logo': logo, + 'reel': reel + } + return channel_map + + def get_last_uploads(self, last_upload_range=12): + current_date = datetime.now() + current_delta = (current_date - timedelta(hours=last_upload_range)) + + today_filter = current_date.strftime("%Y-%m-%d") + yesterday_filter = current_delta.strftime("%Y-%m-%d") + + recently_added = [] + if current_delta.strftime("%d.%m.%Y") != current_date.strftime("%d.%m.%Y"): + self.log("Also fetching videos from yesterday") + request_url = self.api_endpoint_schedule % yesterday_filter + more_uploads = self.get_url(request_url) + for item in more_uploads: + released = item.date() + released_datetime = datetime.fromisoformat(released).replace(tzinfo=None) + if released_datetime > current_delta: + recently_added.append(item) + request_url = self.api_endpoint_schedule % today_filter + uploads = self.get_url(request_url) + for item in uploads: + released = item.date() + released_datetime = datetime.fromisoformat(released).replace(tzinfo=None) + if released_datetime > current_delta: + recently_added.append(item) + return reversed(recently_added) + + def get_schedule_dates(self) -> tuple: + replay_days = self.get_replay_days() + current_date = date.today() + day_items = [] + filter_items = [] + for day in range(replay_days): + days_before = (current_date - timedelta(days=day)) + isodate = days_before.isoformat() + prettydate = days_before.strftime("%A, %d.%m.%Y") + day_items.append(prettydate) + filter_items.append(isodate) + return day_items, filter_items + + def get_frontpage(self, lanes=True) -> list: + url = self.api_endpoint_home + data = self.auth_request(url) + items = self.render(data) + if not lanes: + for item in items[:]: + if item.type() == 'lane': + items.remove(item) + else: + for item in items[:]: + if item.type() != 'lane': + items.remove(item) + return items + + def get_live_schedule(self) -> list: + url = self.api_endpoint_livestreams + data = self.auth_request(url) + streams = [] + for channel in data: + channel_streams = data[channel] + for stream_item in channel_streams['items']: + stream_dir = self.build(stream_item) + stream_dir.set_channel(channel) + if stream_dir and stream_dir.livestream_active(): + streams.append(stream_dir) + return streams + + def get_pvr(self, channel_reel) -> Directory: + channel_infos = self.get_channel_map() + for channel_id in channel_infos: + if channel_infos[channel_id].get('reel') == channel_reel: + request_url = self.api_endpoint_channel_livestream % channel_id + data = self.auth_request(request_url) + if data and '_embedded' in data and 'items' in data['_embedded']: + for livestream_item in data['_embedded']['items']: + stream_dir = self.build(livestream_item) + stream_dir.set_channel(channel_reel) + if stream_dir: + stream_detail_url = stream_dir.url() + stream_detail_data = self.load_stream_data(stream_detail_url) + if stream_detail_data and len(stream_detail_data): + pvr_stream = stream_detail_data[0] + pvr_stream.set_pvr_mode() + return pvr_stream + + def get_search(self, query) -> list: + request_url = self.api_endpoint_search % quote_plus(query) + data = self.auth_request(request_url) + results = [] + if 'search' in data: + if 'episodes' in data['search']: + if data['search']['episodes']['total'] > 0: + title = ' - ' + self.translate_string(30124, 'All episode results') + ' (%d) -' % data['search']['episodes']['total'] + desc = "" + link = self.api_endpoint_search_partial % ('episodes', query, self.api_pager_limit) + results.append(Directory(title, desc, link)) + + if 'segments' in data['search']: + if data['search']['segments']['total'] > 0: + title = ' - ' + self.translate_string(30125, 'All chapter results') + ' (%d) -' % data['search']['segments']['total'] + desc = "" + link = self.api_endpoint_search_partial % ('segments', query, self.api_pager_limit) + results.append(Directory(title, desc, link)) + + if 'history' in data['search']: + if data['search']['history']['total'] > 0: + title = ' - ' + self.translate_string(30126, 'All history results') + ' (%d) -' % data['search']['history']['total'] + desc = "" + link = self.api_endpoint_search_partial % ('history', query, self.api_pager_limit) + results.append(Directory(title, desc, link)) + + if 'suggestions' in data: + if 'episodes' in data['suggestions']: + for episode in data['suggestions']['episodes']: + results.append(self.build(episode)) + if 'segments' in data['suggestions']: + for segment in data['suggestions']['segments']: + results.append(self.build(segment)) + if 'history' in data['suggestions']: + for history in data['suggestions']['history']: + results.append(self.build(history)) + return results + + def get_search_partial(self, section, query, args): + request_url = self.api_endpoint_search_partial % (section, quote_plus(query), self.api_pager_limit) + if args.get('page'): + request_url += "&page=%d" % int(args.get('page')[0]) + data = self.auth_request(request_url) + results = [] + if data and 'items' in data: + for item in data['items']: + results.append(self.build(item)) + if 'next' in data and data['next'] and data['next'] != "": + next_page_url = self.clean_url(data['next']) + results.append(Directory(self.translate_string(30127, 'Next page', '[COLOR blue][B]%s[/B][/COLOR]'), '', next_page_url, '', 'pager')) + return results + + def get_url(self, url) -> list: + data = self.auth_request(url) + return self.render(data) + + def get_listing(self, item) -> list: + url = item.url() + data = self.auth_request(url) + return self.render(data) + + def get_livestream(self, livestream_id) -> Directory: + url = self.api_endpoint_livestream % livestream_id + data = self.auth_request(url) + return self.build(data) + + def get_related(self, episodeid) -> list: + episode_details = self.get_url('/episode/%s' % episodeid) + for episode_detail in episode_details: + episode_source = episode_detail.get_source() + if 'profile' in episode_source.get('_links'): + profile_url = self.clean_url(episode_source.get('_links').get('profile').get('href')) + profile_details = self.get_url(profile_url) + return profile_details + + def get_timeshift_stream_url(self, item) -> str: + if '_embedded' in item.source and 'channel' in item.source['_embedded']: + channel_id = item.source['_embedded']['channel']['id'] + timeshift_url = self.api_endpoint_timeshift % channel_id + timeshift_data = self.auth_request(timeshift_url) + if timeshift_data and 'sources' in timeshift_data and self.supported_delivery in timeshift_data['sources']: + source = timeshift_data['sources'][self.supported_delivery] + source['drm_token'] = timeshift_data['drm_token'] + return source + + def get_restart_stream_url(self, item) -> str: + timeshift_sources = self.get_timeshift_stream_url(item) + if item.has_timeshift() and timeshift_sources: + start_time = item.get_start_time_iso() + return "%s?begin=%s" % (timeshift_sources['src'], start_time) + + def get_restart_stream(self, item) -> Directory: + source = self.get_timeshift_stream_url(item) + start_time = item.get_start_time_iso() + item.set_stream({ + 'url': "%s&begin=%s" % (source['src'], start_time), + 'drm': source['is_drm_protected'], + 'drm_token': source['drm_token'], + 'drm_widewine_url': self.get_widevine_url(), + 'drm_widewine_brand': self.get_widevine_brand(True) + }) + return item + + def get_subtitle_url(self, playitem, subtitle_type='srt'): + if '_links' in playitem and 'subtitle' in playitem['_links']: + subtitle_url = self.clean_url(playitem['_links']['subtitle']['href']) + data = self.auth_request(subtitle_url) + if data and '%s_url' % subtitle_type in data: + return data['%s_url' % subtitle_type] + + def load_stream_data(self, url) -> list: + self.log("Loading Stream Details from %s" % url) + data = self.auth_request(url) + + playlist = [] + if '_embedded' in data and 'items' in data['_embedded']: + for playitem in data['_embedded']['items']: + source = self.get_preferred_source(playitem) + if source: + video = self.build_video(playitem, source['src']) + video.set_stream({ + 'url': source['src'], + 'drm': source['is_drm_protected'], + 'drm_token': playitem['drm_token'], + 'drm_widewine_url': self.get_widevine_url(), + 'drm_widewine_brand': self.get_widevine_brand(), + 'subtitle': self.get_subtitle_url(playitem, 'srt') + }) + playlist.append(video) + elif 'segments' in playitem.get('_embedded'): + for segment in playitem.get('_embedded').get('segments'): + source = self.get_preferred_source(segment) + if source: + video = self.build_video(segment, source['src']) + video.set_stream({ + 'url': source['src'], + 'drm': source['is_drm_protected'], + 'drm_token': segment['drm_token'], + 'drm_widewine_url': self.get_widevine_url(), + 'drm_widewine_brand': self.get_widevine_brand(), + 'subtitle': self.get_subtitle_url(segment, 'srt') + }) + playlist.append(video) + elif '_embedded' in data and 'item' in data['_embedded']: + item = data['_embedded']['item'] + source = self.get_preferred_source(item) + if not source: + self.log("No video available yet.") + return [] + video = self.build_video(item, source['src']) + video.set_stream({ + 'url': source['src'], + 'drm': source['is_drm_protected'], + 'drm_token': item['drm_token'], + 'drm_widewine_url': self.get_widevine_url(), + 'drm_widewine_brand': self.get_widevine_brand(), + 'subtitle': self.get_subtitle_url(item, 'srt') + }) + playlist.append(video) + elif 'sources' in data: + source = self.get_preferred_source(data) + if not source: + self.log("No video available yet.") + return [] + video = self.build_video(data, source['src']) + if self.kodi_worker.use_timeshift and '_embedded' in data and 'channel' in data['_embedded'] and data['timeshift_available_livestream']: + source = self.get_timeshift_stream_url(video) + start_time = video.get_start_time_iso() + ts_url = "%s&begin=%s" % (source['src'], start_time) + video.set_url(ts_url) + video.set_stream({ + 'url': ts_url, + 'drm': source['is_drm_protected'], + 'drm_token': source['drm_token'], + 'drm_widewine_url': self.get_widevine_url(), + 'drm_widewine_brand': self.get_widevine_brand(True), + 'subtitle': self.get_subtitle_url(data, 'srt') + }) + else: + video.set_stream({ + 'url': source['src'], + 'drm': source['is_drm_protected'], + 'drm_token': data['drm_token'], + 'drm_widewine_url': self.get_widevine_url(), + 'drm_widewine_brand': self.get_widevine_brand(), + 'subtitle': self.get_subtitle_url(data, 'srt') + }) + playlist.append(video) + return playlist + + def get_preferred_source(self, item): + if self.supported_delivery in item['sources']: + for source in item['sources'][self.supported_delivery]: + for quality in self.quality_definitions: + if quality in source['quality_key']: + self.log("Found Stream %s" % self.quality_definitions[quality]['name']) + return source + + def render(self, data) -> list: + content = [] + if isinstance(data, list): + for item in data: + result = self.build(item) + if result: + content.append(result) + + elif 'page' in data and '_items' in data: + item = {} + for item in data['_items']: + result = self.build(item) + if result: + content.append(result) + if 'next' in item['_links']: + next_page_url = self.clean_url(item['_links']['next']['href']) + content.append(Directory(self.translate_string(30127, 'Next page', '[COLOR blue][B]%s[/B][/COLOR]'), '', next_page_url, '', 'pager')) + + elif 'page' in data and '_embedded' in data and 'items' in data['_embedded']: + for item in data['_embedded']['items']: + result = self.build(item) + if result: + content.append(result) + + if 'next' in data['_links']: + next_page_url = self.clean_url(data['_links']['next']['href']) + content.append(Directory(self.translate_string(30127, 'Next page', '[COLOR blue][B]%s[/B][/COLOR]'), '', next_page_url, '', 'pager')) + + elif 'history_highlights' in data: + for item in data['history_highlights']: + result = self.build(item) + if result: + content.append(result) + for item in data['history_items']: + result = self.build(item) + if result: + content.append(result) + + elif 'timeShift' in data: + for item in data['timeShift']: + result = self.build(data['timeShift'][item]) + if result: + content.append(result) + + elif 'children_count' in data: + if data['children_count'] > 0: + for item in data['children']: + result = self.build(item) + if result: + content.append(result) + else: + for item in data['video_items']['_items']: + result = self.build(item) + if result: + content.append(result) + elif '_links' in data and 'episodes' in data['_links']: + episode_url = self.clean_url(data['_links']['episodes']['href']) + return self.get_url(episode_url) + elif isinstance(data, dict) and 'video_type' in data: + result = self.build(data) + if result: + content.append(result) + else: + self.log("Unknown Render Type", 'error') + self.print_obj(data) + + return content + + def build(self, item) -> Directory: + if '_embedded' in item and 'video_item' in item['_embedded']: + video_item = item['_embedded']['video_item']['_embedded']['item'] + link = item['_embedded']['video_item']['_links']['self']['href'] + return self.build_video(video_item, link) + elif 'sources' in item and 'segments' in item['_links']: + link = item['_links']['segments']['href'] + return self.build_video(item, link) + elif 'sources' in item and 'playlist' in item['_links']: + link = item['_links']['playlist']['href'] + return self.build_video(item, link) + elif 'id' in item and 'type' in item: + return self.build_directory(item) + elif 'id' in item and 'videos' in item: + return self.build_directory(item) + elif 'video_type' in item: + video_item = item + link = item['_links']['self']['href'] + return self.build_video(video_item, link) + else: + self.log("Unknown Type", 'error') + self.print_obj(item) + + def build_directory(self, item) -> Directory: + self.log("Building Directory %s (%s)" % (item['title'], item['id'])) + banner, backdrop, poster = self.get_images(item) + item['channel_meta'] = self.channel_map + item_id = item['id'] + if 'type' in item: + item_type = item['type'] + else: + item_type = 'generic' + + if 'description' in item and item['description'] is not None and item['description'] != "": + description = item['description'] + elif 'share_subject' in item: + description = item['share_subject'] + elif 'episode_title' in item: + description = item['episode_title'] + else: + description = "" + + if 'self' in item['_links'] and 'href' in item['_links']['self']: + link = self.clean_url(item['_links']['self']['href']) + elif '_self' in item['_links'] and isinstance(item['_links']['_self'], str): + link = self.clean_url(item['_links']['_self']) + else: + link = self.clean_url(item['_links']['self']) + + if item_type == 'genre': + link = "%s/profiles?limit=%d" % (link, self.api_pager_limit) + return Directory(item['title'], description, link, item['id'], item['type'], banner, backdrop, poster, item, translator=self.kodi_worker) + elif item_id == 'lane': + return Directory(item['title'], description, link, item['id'], item['type'], banner, backdrop, poster, item, translator=self.kodi_worker) + elif item_id == 'highlights': + return Directory(self.type_map['highlights'], description, link, item['id'], item['type'], banner, backdrop, poster, item, translator=self.kodi_worker) + elif item_id == 'genres': + return Directory(self.type_map['genres'], description, link, item['id'], item['type'], banner, backdrop, poster, item, translator=self.kodi_worker) + elif item_id == 'orflive': + return Directory(self.type_map['orflive'], description, link, item['id'], item['type'], banner, backdrop, poster, item, translator=self.kodi_worker) + elif 'title' in item and item['title'] and 'type' in item: + return Directory(item['title'], description, link, item['id'], item['type'], banner, backdrop, poster, item, translator=self.kodi_worker) + elif 'title' in item and item['title'] and 'children_count' in item: + return Directory(item['title'], description, link, item['id'], 'directory', banner, backdrop, poster, item, translator=self.kodi_worker) + + def build_video(self, item, link) -> Directory: + self.log("Building Video %s (%s)" % (item['title'], item['id'])) + title = item['title'] + link = self.clean_url(link) + + # Try to get the segements if available and activated for the api. + if self.use_segments: + if 'segments_complete' in item and 'video_type' in item and item['video_type'] == 'episode' and '/segments' not in link and 'episode' in link: + self.log("Found video with segments.") + link = self.clean_url(link + "/segments") + else: + if 'episode' in link and link.endswith('/segments'): + link = link.replace('/segments', '') + + if 'description' in item and item['description'] is not None and item['description'] != "": + description = item['description'] + elif 'share_subject' in item: + description = item['share_subject'] + elif 'episode_title' in item: + description = item['episode_title'] + else: + description = "" + video_type = item['video_type'] + video_id = item['id'] + banner, backdrop, poster = self.get_images(item) + item['channel_meta'] = self.channel_map + self.log("Video Link %s" % link) + return Directory(title, description, link, video_id, video_type, banner, backdrop, poster, item, translator=self.kodi_worker) + + def clean_url(self, url): + return url.replace(self.api_base, "") + + def get_images(self, item) -> tuple: + try: + if '_embedded' in item: + banner = item['_embedded']['image']['public_urls']['highlight_teaser']['url'] + backdrop = item['_embedded']['image']['public_urls']['reference']['url'] + if 'image2x3_with_logo' in item['_embedded'] and '_default_' not in item['_embedded']['image2x3_with_logo']['public_urls']['highlight_teaser']['url']: + poster = item['_embedded']['image2x3_with_logo']['public_urls']['highlight_teaser']['url'] + elif '_default_' not in item['_embedded']['image2x3']['public_urls']['highlight_teaser']['url']: + poster = item['_embedded']['image2x3']['public_urls']['highlight_teaser']['url'] + else: + poster = banner + return banner, backdrop, poster + except IndexError: + self.log("No images found for %s (%s)" % (item['title'], item['id']), 'warning') + except KeyError: + self.log("No images found for %s (%s)" % (item['title'], item['id']), 'warning') + return "", "", "" + + def log(self, msg, msg_type='info'): + self.kodi_worker.log("[%s][ORFON][API] %s" % (msg_type.upper(), msg)) + + def print_obj(self, obj): + self.log(json.dumps(obj, indent=4)) diff --git a/resources/lib/Scraper.py b/resources/lib/Scraper.py deleted file mode 100644 index 7ee0f4a..0000000 --- a/resources/lib/Scraper.py +++ /dev/null @@ -1,45 +0,0 @@ -import abc - - -class Scraper(object): - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def getCategories(self): - pass - - @abc.abstractmethod - def getHighlights(self): - pass - - @abc.abstractmethod - def getLiveStreams(self): - pass - - @abc.abstractmethod - def getMostViewed(self): - pass - - @abc.abstractmethod - def getNewest(self): - pass - - @abc.abstractmethod - def getThemen(self): - pass - - @abc.abstractmethod - def getTips(self): - pass - - @abc.abstractmethod - def getSchedule(self): - pass - - @abc.abstractmethod - def getArchiv(self): - pass - - @abc.abstractmethod - def getLivestreamByChannel(self, channel): - pass diff --git a/resources/lib/ServiceApi.py b/resources/lib/ServiceApi.py deleted file mode 100644 index 5bdf0d8..0000000 --- a/resources/lib/ServiceApi.py +++ /dev/null @@ -1,551 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -import datetime -import time -import sys -import re - -PY3 = sys.version_info.major >=3 -if PY3: - from urllib.error import HTTPError -else: - from urllib2 import HTTPError - -from .Base import * -from .Scraper import * - - -class serviceAPI(Scraper): - __urlBase = 'https://api-tvthek.orf.at/api/v3/' - __urlBaseV4 = 'https://api-tvthek.orf.at/api/v4.2/' - __urlLive = 'livestreams/24hours?limit=20' - __urlLiveChannels = 'livestreams' - __urlMostViewed = 'page/startpage' - __urlNewest = 'page/startpage/newest' - __urlSearch = __urlBase + 'search/%s?limit=1000' - __urlShows = 'profiles?limit=1000' - __urlTips = 'page/startpage/tips' - __urlTopics = 'topics/overview?limit=1000' - __urlChannel = 'channel/' - __urlDRMLic = 'https://drm.ors.at/acquire-license/widevine' - __brandIdDRM = '13f2e056-53fe-4469-ba6d-999970dbe549' - __bundeslandMap = { - 'orf2b': 'Burgenland', - 'orf2stmk': 'Steiermark', - 'orf2w': 'Wien', - 'orf2ooe': 'Oberösterreich', - 'orf2k': 'Kärnten', - 'orf2n': 'Niederösterreich', - 'orf2s': 'Salzburg', - 'orf2v': 'Vorarlberg', - 'orf2t': 'Tirol', - } - __channelMap = { - 'orf1': 'ORF 1', - 'orf2': 'ORF 2', - 'orf3': 'ORF III', - 'orfs': 'ORF Sport+', - 'live_special': 'Special' - } - - serviceAPIEpisode = 'episode/%s' - serviceAPIDate = 'schedule/%s?limit=1000' - serviceAPIDateFrom = 'schedule/%s/%d?limit=1000' - serviceAPIProgram = 'profile/%s/episodes' - servieAPITopic = 'topic/%s' - serviceAPITrailers = 'page/preview?limit=100' - serviceAPIHighlights = 'page/startpage' - - httpauth = 'cHNfYW5kcm9pZF92M19uZXc6MDY1MmU0NjZkMTk5MGQxZmRmNDBkYTA4ZTc5MzNlMDY==' - - def __init__(self, xbmc, settings, pluginhandle, quality, protocol, delivery, defaultbanner, defaultbackdrop, usePlayAllPlaylist): - self.translation = settings.getLocalizedString - self.xbmc = xbmc - self.videoQuality = quality - self.videoDelivery = delivery - self.videoProtocol = protocol - self.pluginhandle = pluginhandle - self.defaultbanner = defaultbanner - self.defaultbackdrop = defaultbackdrop - self.usePlayAllPlaylist = usePlayAllPlaylist - debugLog('ServiceAPI - Init done', xbmc.LOGDEBUG) - - def getLivestreamByChannel(self, channel): - response = self.__makeRequestV4(self.__urlLiveChannels) - response_raw = response.read().decode('UTF-8') - channels = json.loads(response_raw) - - live_link = False - for result in channels: - if channel in self.__bundeslandMap: - channel_items = channels[result].get('items') - for channel_item in channel_items: - if channel_item.get('title') == "%s heute" % self.__bundeslandMap[channel]: - live_link = channel_item.get('_links').get('self').get('href') - if result == channel or channel in self.__bundeslandMap and result == channel[0:4] and not live_link: - live_link = channels[result].get('items')[0].get('_links').get('self').get('href') - - if live_link: - response = url_get_request(live_link, self.httpauth) - response_raw = response.read().decode('UTF-8') - live_json = json.loads(response_raw) - if live_json.get('is_drm_protected'): - video_url = self.JSONStreamingDrmURL(live_json) - uhd_25_video_url = self.JSONStreamingDrmURL(live_json, 'uhdbrowser') - if uhd_25_video_url: - video_url = uhd_25_video_url; - uhd_50_video_url = self.JSONStreamingDrmURL(live_json, 'uhdsmarttv') - if uhd_50_video_url: - video_url = uhd_50_video_url - license_url = self.JSONLicenseDrmURL(live_json) - return {'title': live_json.get('title'), 'description': live_json.get('share_subject'), 'url': video_url,'license': license_url} - else: - video_url = self.JSONStreamingURL(live_json.get('sources')) - return {'title': live_json.get('title'), 'description': live_json.get('share_subject'), 'url': video_url} - - def getHighlights(self): - try: - response = self.__makeRequest(self.serviceAPIHighlights) - responseCode = response.getcode() - except HTTPError as error: - responseCode = error.getcode() - - if responseCode == 200: - for result in json.loads(response.read().decode('UTF-8')).get('highlight_teasers'): - if result.get('target').get('model') == 'Segment' or result.get('target').get('model') == 'Episode': - self.JSONSegment2ListItem(result.get('target')) - - def getMostViewed(self): - try: - response = self.__makeRequest(self.__urlMostViewed) - responseCode = response.getcode() - except HTTPError as error: - responseCode = error.getcode() - - if responseCode == 200: - for result in json.loads(response.read().decode('UTF-8')).get('most_viewed_segments'): - if result.get('model') == 'Segment': - self.JSONSegment2ListItem(result) - - def getNewest(self): - self.getTableResults(self.__urlNewest) - - def getTips(self): - self.getTableResults(self.__urlTips) - - def getFocus(self): - debugLog('"In Focus" not available', level=xbmc.LOGDEBUG) - - def getTableResults(self, urlAPI): - try: - response = self.__makeRequest(urlAPI) - responseCode = response.getcode() - except HTTPError as error: - responseCode = error.getcode() - - if responseCode == 200: - for result in json.loads(response.read().decode('UTF-8')): - if result.get('model') == 'Episode': - self.__JSONEpisode2ListItem(result) - elif result.get('model') == 'Tip': - self.__JSONVideoItem2ListItem(result.get('_embedded').get('video_item')) - - else: - debugLog('ServiceAPI not available for %s ... switch back to HTML Parsing in the Addon Settings' % urlAPI, level=xbmc.LOGDEBUG) - showDialog(self.translation(30045).encode('UTF-8'), self.translation(30046).encode('UTF-8')) - - # Useful Methods for JSON Parsing - def JSONSegment2ListItem(self, JSONSegment): - if JSONSegment.get('killdate') is not None and time.strptime(JSONSegment.get('killdate')[0:19], '%Y-%m-%dT%H:%M:%S') < time.localtime(): - return - title = JSONSegment.get('title').encode('UTF-8') - image = self.JSONImage(JSONSegment) - description = JSONSegment.get('description') - duration = JSONSegment.get('duration_seconds') - if JSONSegment.get('episode_date'): - date = time.strptime(JSONSegment.get('episode_date')[0:19], '%Y-%m-%dT%H:%M:%S') - elif JSONSegment.get('date'): - date = time.strptime(JSONSegment.get('date')[0:19], '%Y-%m-%dT%H:%M:%S') - else: - date = "" - streamingURL = self.JSONStreamingURL(JSONSegment.get('sources')) - if JSONSegment.get('playlist').get('subtitles'): - subtitles = [x.get('src') for x in JSONSegment.get('playlist').get('subtitles')] - else: - subtitles = [] - return [streamingURL, createListItem(title, image, description, duration, time.strftime('%Y-%m-%d', date), '', streamingURL, True, False, self.defaultbackdrop, self.pluginhandle, subtitles)] - - @staticmethod - def JSONImage(jsonImages, name='image_full'): - if jsonImages.get('playlist'): - return jsonImages.get('playlist').get('preview_image_url') - return "" - - def JSONStreamingURL(self, jsonVideos): - source = None - if jsonVideos.get('progressive_download') is not None: - for streamingUrl in jsonVideos.get('progressive_download'): - if streamingUrl.get('quality_key') == self.videoQuality: - return generateAddonVideoUrl(streamingUrl.get('src')) - source = streamingUrl.get('src') - - for streamingUrl in jsonVideos.get('hls'): - if streamingUrl.get('quality_key') == self.videoQuality: - # Remove Get Parameters because InputStream Adaptive cant handle it. - source = re.sub(r"\?[\S]+", '', streamingUrl.get('src'), 0) - return generateAddonVideoUrl(source) - source = re.sub(r"\?[\S]+", '', streamingUrl.get('src'), 0) - if source is not None: - return generateAddonVideoUrl(source) - else: - showDialog(self.translation(30014).encode('UTF-8'), self.translation(30050).encode('UTF-8')) - return - - def JSONLicenseDrmURL(self, jsonData): - if jsonData.get('drm_token') is not None: - token = jsonData.get('drm_token') - license_url = "%s?BrandGuid=%s&userToken=%s" % (self.__urlDRMLic, self.__brandIdDRM, token) - debugLog("DRM License Url %s" % license_url) - return license_url - - def JSONStreamingDrmURL(self, jsonData, uhd_profile = False): - if jsonData.get('drm_token') is not None: - license_url = self.JSONLicenseDrmURL(jsonData) - jsonVideos = jsonData.get('sources') - - if uhd_profile: - for streamingUrl in jsonVideos.get('dash'): - if streamingUrl.get('is_uhd') and streamingUrl.get('quality_key').lower() == uhd_profile: - source = re.sub(r"\?[\S]+", '', streamingUrl.get('src'), 0) - return generateDRMVideoUrl(source, license_url) - return False - - for streamingUrl in jsonVideos.get('dash'): - if streamingUrl.get('quality_key').lower()[0:3] == self.videoQuality: - return generateDRMVideoUrl(streamingUrl.get('src'), license_url) - source = streamingUrl.get('src') - # Remove Get Parameters because InputStream Adaptive cant handle it. - source = re.sub(r"\?[\S]+", '', source, 0) - if source is not None: - return generateDRMVideoUrl(source, license_url) - else: - showDialog(self.translation(30014).encode('UTF-8'), self.translation(30050).encode('UTF-8')) - return - - # list all Categories - def getCategories(self): - try: - response = self.__makeRequest(self.__urlShows) - responseCode = response.getcode() - except HTTPError as error: - responseCode = error.getcode() - - if responseCode == 200: - for result in json.loads(response.read().decode('UTF-8')).get('_embedded').get('items'): - self.__JSONProfile2ListItem(result) - else: - showDialog(self.translation(30045).encode('UTF-8'), self.translation(30046).encode('UTF-8')) - - # list all Episodes for the given Date - def getDate(self, date, dateFrom=None): - if dateFrom is None: - url = self.serviceAPIDate % date - else: - url = self.serviceAPIDateFrom % (date, 7) - response = self.__makeRequest(url) - - episodes = json.loads(response.read().decode('UTF-8')).get('_embedded').get('items') - if dateFrom is not None: - episodes = reversed(episodes) - - for episode in episodes: - self.__JSONEpisode2ListItem(episode) - - # list all Entries for the given Topic - def getTopic(self, topicID): - response = self.__makeRequest(self.servieAPITopic % topicID) - for entrie in json.loads(response.read().decode('UTF-8')).get('_embedded').get('video_items'): - self.__JSONVideoItem2ListItem(entrie) - - # list all Episodes for the given Broadcast - def getProgram(self, programID, playlist): - response = self.__makeRequest(self.serviceAPIProgram % programID) - responseCode = response.getcode() - - if responseCode == 200: - episodes = json.loads(response.read().decode('UTF-8')).get('_embedded').get('items') - if len(episodes) == 1: - for episode in episodes: - self.getEpisode(episode.get('id'), playlist) - return - - for episode in episodes: - self.__JSONEpisode2ListItem(episode, 'teaser') - else: - showDialog(self.translation(30045).encode('UTF-8'), self.translation(30046).encode('UTF-8')) - - # listst all Segments for the Episode with the given episodeID - # If the Episode only contains one Segment, that get played instantly. - def getEpisode(self, episodeID, playlist): - playlist.clear() - - response = self.__makeRequest(self.serviceAPIEpisode % episodeID) - result = json.loads(response.read().decode('UTF-8')) - - if len(result.get('_embedded').get('segments')) == 1: - listItem = self.JSONSegment2ListItem(result.get('_embedded').get('segments')[0]) - playlist.add(listItem[0], listItem[1]) - else: - gapless_name = '-- %s --' % self.translation(30059) - streamingURL = self.JSONStreamingURL(result.get('sources')) - description = result.get('description') - duration = result.get('duration_seconds') - teaser_image = result.get('playlist').get('preview_image_url') - date = time.strptime(result.get('date')[0:19], '%Y-%m-%dT%H:%M:%S') - if result.get('playlist').get('is_gapless'): - subtitles = [x.get('src') for x in result.get('playlist').get('gapless_video').get('subtitles')] - createListItem(gapless_name, teaser_image, description, duration, time.strftime('%Y-%m-%d', date), '', streamingURL, True, False, self.defaultbackdrop, self.pluginhandle, subtitles) - - if self.usePlayAllPlaylist: - play_all_name = '-- %s --' % self.translation(30060) - stream_infos = { - 'teaser_image': teaser_image, - 'description': description - } - createPlayAllItem(play_all_name, self.pluginhandle, stream_infos) - - for segment in result.get('_embedded').get('segments'): - listItem = self.JSONSegment2ListItem(segment) - playlist.add(listItem[0], listItem[1]) - - # Parses the Topic Overview Page - def getThemen(self): - try: - response = self.__makeRequest(self.__urlTopics) - responseCode = response.getcode() - except ValueError as error: - responseCode = 404 - except HTTPError as error: - responseCode = error.getcode() - - if responseCode == 200: - for topic in json.loads(response.read().decode('UTF-8')).get('_embedded').get('items'): - title = topic.get('title').encode('UTF-8') - description = topic.get('description') - link = topic.get('id') - addDirectory(title, None, self.defaultbackdrop, description, link, 'openTopic', self.pluginhandle) - - else: - showDialog(self.translation(30045).encode('UTF-8'), self.translation(30046).encode('UTF-8')) - - # list all Trailers for further airings - def getTrailers(self): - try: - response = self.__makeRequest(self.serviceAPITrailers) - responseCode = response.getcode() - except ValueError as error: - responseCode = 404 - except HTTPError as error: - responseCode = error.getcode() - - if responseCode == 200: - for episode in json.loads(response.read().decode('UTF-8'))['_embedded']['items']: - self.__JSONEpisode2ListItem(episode) - else: - showDialog(self.translation(30045).encode('UTF-8'), self.translation(30046).encode('UTF-8')) - - def getArchiv(self): - pass - - # lists schedule overview (date listing) - def getSchedule(self): - for x in range(9): - date = datetime.datetime.now() - datetime.timedelta(days=x) - title = '%s' % (date.strftime('%A, %d.%m.%Y')) - parameters = {'mode': 'openDate', 'link': date.strftime('%Y-%m-%d')} - if x == 8: - title = '%s %s' % (self.translation(30064), title) - parameters = {'mode': 'openDate', 'link': date.strftime('%Y-%m-%d'), 'from': (date - datetime.timedelta(days=150)).strftime('%Y-%m-%d')} - u = build_kodi_url(parameters) - createListItem(title, None, None, None, date.strftime('%Y-%m-%d'), '', u, False, True, self.defaultbackdrop, self.pluginhandle) - - # Fetch stream details. - def getStreamInfos(self, item, inputstreamAdaptive): - infos = {} - live_link = item.get('_links').get('self').get('href') - response = url_get_request(live_link, self.httpauth) - response_raw = response.read().decode('UTF-8') - infos['live'] = json.loads(response_raw) - infos['drmurl'] = self.JSONLicenseDrmURL(infos['live']) - if inputstreamAdaptive and infos['live'].get('is_drm_protected'): - infos['stream'] = self.JSONStreamingDrmURL(infos['live']) - else: - infos['stream'] = self.JSONStreamingURL(infos['live'].get('sources')) - - infos['uhd25_stream'] = self.JSONStreamingDrmURL(infos['live'], 'uhdbrowser') - infos['uhd50_stream'] = self.JSONStreamingDrmURL(infos['live'], 'uhdsmarttv') - infos['items'] = {} - return infos - - # Builds a livestream item. - def buildStreamItem(self, item, channel, stream_imfo, inputstreamAdaptive, use_restart=True): - description = item.get('description') - title = item.get('title') - programName = channel - if channel in self.__channelMap: - programName = self.__channelMap[channel] - livestreamStart = time.strptime(item.get('start')[0:19], '%Y-%m-%dT%H:%M:%S') - livestreamEnd = time.strptime(item.get('end')[0:19], '%Y-%m-%dT%H:%M:%S') - duration = max(time.mktime(livestreamEnd) - max(time.mktime(livestreamStart), time.mktime(time.localtime())), 1) - contextMenuItems = [] - restart_url = False - if inputstreamAdaptive and item.get('restart'): - restart_parameters = {"mode": "liveStreamRestart", "link": item.get('id'), "lic_url": stream_imfo['drmurl']} - restart_url = build_kodi_url(restart_parameters) - contextMenuItems.append((self.translation(30063), 'RunPlugin(%s)' % restart_url)) - - banner = self.JSONImage(item.get('_embedded').get('image')) - item_title = "[%s] %s %s (%s)" % (programName, "[%s]" % self.translation(30063) if inputstreamAdaptive and restart_url else '', title, time.strftime('%H:%M', livestreamStart)) - if item.get('uhd') and stream_imfo['uhd25_stream']: - createListItem("[UHD] %s" % item_title, banner, description, duration,time.strftime('%Y-%m-%d', livestreamStart), programName, stream_imfo['uhd25_stream'], True, False, self.defaultbackdrop, self.pluginhandle) - if item.get('uhd') and stream_imfo['uhd50_stream']: - createListItem("[UHD 50fps] %s" % item_title, banner, description, duration,time.strftime('%Y-%m-%d', livestreamStart), programName, stream_imfo['uhd50_stream'], True, False, self.defaultbackdrop, self.pluginhandle) - - createListItem(item_title, banner, description, duration, time.strftime('%Y-%m-%d', livestreamStart), programName, stream_imfo['stream'], True, False, self.defaultbackdrop, self.pluginhandle, contextMenuItems=contextMenuItems) - - # Returns Live Stream Listing - def getLiveStreams(self): - try: - xbmcaddon.Addon('inputstream.adaptive') - inputstreamAdaptive = True - except RuntimeError: - inputstreamAdaptive = False - - showFullSchedule = xbmcaddon.Addon().getSetting('showLiveStreamSchedule') == 'true' - - response = self.__makeRequestV4(self.__urlLiveChannels) - response_raw = response.read().decode('UTF-8') - channels = json.loads(response_raw) - channelresults = {} - - # Prefetch the stream infos to limit request for each stream. - for channel in channels: - channelresults[channel] = {} - channel_items = channels[channel].get('items') - for channel_item in channel_items: - channelresults[channel] = self.getStreamInfos(channel_item, inputstreamAdaptive) - channelresults[channel]['items'] = channel_items - break - - # Render current streams first. - for channel in channels: - for upcoming in channelresults[channel]['items']: - if not 'upcoming' in channels[channel] or ('upcoming' in channels[channel] and upcoming.get('start')[0:17] == channels[channel]['upcoming'].get('start')[0:17]): - if not 'upcoming' in channels[channel]: - channels[channel]['upcoming'] = upcoming - elif upcoming.get('start')[0:17] == channels[channel]['upcoming'].get('start')[0:17] and upcoming.get('id') != channels[channel]['upcoming'].get('id'): - channelresults[channel] = self.getStreamInfos(upcoming, inputstreamAdaptive) - self.buildStreamItem(upcoming, channel, channelresults[channel], inputstreamAdaptive) - - # Render upcoming streams last for better list item order. - if showFullSchedule: - addDirectory('[COLOR red]----------------[/COLOR]', None, self.defaultbackdrop, "", "", 'getLive', self.pluginhandle) - for channel in channels: - for upcoming in channelresults[channel]['items']: - if not 'upcoming' in channels[channel] or channels[channel]['upcoming'].get('id') != upcoming.get('id'): - self.buildStreamItem(upcoming, channel, channelresults[channel], inputstreamAdaptive, False) - - # Restart callback. - def liveStreamRestart(self, link, protocol): - try: - xbmcaddon.Addon('inputstream.adaptive') - except RuntimeError: - return - - try: - response = self.__makeRequest('livestream/' + link) - responseCode = response.getcode() - except HTTPError as error: - responseCode = error.getcode() - - if responseCode == 200: - result = json.loads(response.read().decode('UTF-8')) - title = result.get('title').encode('UTF-8') - image = self.JSONImage(result.get('_embedded').get('image')) - description = result.get('description') - duration = result.get('duration_seconds') - date = time.strptime(result.get('start')[0:19], '%Y-%m-%dT%H:%M:%S') - restart_urls = None - restart_url = None - try: - restart_urls = result['_embedded']['channel']['restart_urls'] - except AttributeError: - pass - else: - for x in ('android', 'default'): - if x in restart_urls: - restart_url = restart_urls[x] - if restart_url: - break - if not restart_url: - raise Exception("restart url not found in livestream/%s result" % (link, )) - m = re.search(r"/livestreams/([^/]+)/sections/[^\?]*\?(?:.+&|)?X-Api-Key=([^&]+)", restart_url) - if m: - bitmovinStreamId, ApiKey = m.groups() - else: - raise Exception("unable to parse restart url: %s" % ( restart_url, )) - response = url_get_request(restart_url) # nosec - response_raw = response.read().decode('UTF-8') - section = json.loads(response_raw)[0] - section_id = section.get('id') - timestamp = section.get('metaData').get('timestamp') - streamingURL = 'https://playerapi-restarttv.ors.at/livestreams/%s/sections/%s/manifests/%s/?startTime=%s&X-Api-Key=%s' % (bitmovinStreamId, section_id, protocol, timestamp, ApiKey) - listItem = createListItem(title, image, description, duration, time.strftime('%Y-%m-%d', date), result.get('SSA').get('channel').upper(), streamingURL, True, False, self.defaultbackdrop, self.pluginhandle) - return streamingURL, listItem - - def __makeRequest(self, url): - return url_get_request(self.__urlBase + url, self.httpauth) - - def __makeRequestV4(self, url): - return url_get_request(self.__urlBaseV4 + url, self.httpauth) - - def __JSONEpisode2ListItem(self, JSONEpisode, ignoreEpisodeType=None): - if JSONEpisode.get('killdate') is not None and time.strptime(JSONEpisode.get('killdate')[0:19], '%Y-%m-%dT%H:%M:%S') < time.localtime(): - return - - # Direcotory should be set to False, that the Duration is shown, but then there is an error with the Pluginhandle - createListItem( - JSONEpisode.get('title'), - self.JSONImage(JSONEpisode), - JSONEpisode.get('description'), - JSONEpisode.get('duration_seconds'), - time.strftime('%Y-%m-%d', time.strptime(JSONEpisode.get('date')[0:19], '%Y-%m-%dT%H:%M:%S')), - JSONEpisode.get('_embedded').get('channel').get('name') if JSONEpisode.get('_embedded').get('channel') is not None else None, - build_kodi_url({'mode': 'openEpisode', 'link': JSONEpisode.get('id')}), - False, - True, - self.defaultbackdrop, - self.pluginhandle, - ) - - def __JSONProfile2ListItem(self, jsonProfile): - createListItem( - jsonProfile.get('title'), - self.JSONImage(jsonProfile), - jsonProfile.get('description'), - None, - None, - None, - build_kodi_url({'mode': 'openProgram', 'link': jsonProfile.get('id')}), - False, - True, - self.defaultbackdrop, - self.pluginhandle - ) - - def __JSONVideoItem2ListItem(self, jsonVideoItem): - if jsonVideoItem.get('_embedded').get('episode') is not None: - self.__JSONEpisode2ListItem(jsonVideoItem.get('_embedded').get('episode')) - elif jsonVideoItem.get('_embedded').get('segment') is not None: - self.JSONSegment2ListItem(jsonVideoItem.get('_embedded').get('segment')) diff --git a/resources/lib/Settings.py b/resources/lib/Settings.py deleted file mode 100644 index af065e6..0000000 --- a/resources/lib/Settings.py +++ /dev/null @@ -1,38 +0,0 @@ -import xbmcaddon - -__addon__ = xbmcaddon.Addon() - - -def blacklist(): - return __addon__.getSetting('enableBlacklist') == 'true' - - -def forceView(): - return __addon__.getSetting('forceView') == 'true' - - -def localizedString(translation_id): - return __addon__.getLocalizedString(translation_id) - - -def serviceAPI(): - return __addon__.getSetting('useServiceAPI') == 'true' - - -def subtitles(): - return __addon__.getSetting('useSubtitles') == 'true' - - -def userAgent(): - return __addon__.getSetting('userAgent') - - -def autoPlayPrompt(): - return __addon__.getSetting("autoPlayPrompt") == "true" - - -def playAllPlaylist(): - return __addon__.getSetting('usePlayAllPlaylist') == 'true' - -def showWarning(): - return __addon__.getSetting('showWarning') == 'true' diff --git a/default.py b/resources/lib/default.py similarity index 63% rename from default.py rename to resources/lib/default.py index 8f5ab32..2c24f58 100644 --- a/default.py +++ b/resources/lib/default.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from resources.lib import Addon +import Addon Addon.run() diff --git a/resources/media/archive_banner_v2.jpg b/resources/media/archive_banner_v2.jpg deleted file mode 100644 index 4479ffa..0000000 Binary files a/resources/media/archive_banner_v2.jpg and /dev/null differ diff --git a/resources/media/blacklist_banner_v2.jpg b/resources/media/blacklist_banner_v2.jpg deleted file mode 100644 index 6b55cf8..0000000 Binary files a/resources/media/blacklist_banner_v2.jpg and /dev/null differ diff --git a/resources/media/default_banner_v2.jpg b/resources/media/default_banner_v2.jpg deleted file mode 100644 index 78968e4..0000000 Binary files a/resources/media/default_banner_v2.jpg and /dev/null differ diff --git a/resources/media/fanart_v2.jpg b/resources/media/fanart_v2.jpg deleted file mode 100644 index d0c00d9..0000000 Binary files a/resources/media/fanart_v2.jpg and /dev/null differ diff --git a/resources/media/focus_banner_v2.jpg b/resources/media/focus_banner_v2.jpg deleted file mode 100644 index 42e2d83..0000000 Binary files a/resources/media/focus_banner_v2.jpg and /dev/null differ diff --git a/resources/media/live_banner_v2.jpg b/resources/media/live_banner_v2.jpg deleted file mode 100644 index c5e1ef1..0000000 Binary files a/resources/media/live_banner_v2.jpg and /dev/null differ diff --git a/resources/media/most_popular_banner_v2.jpg b/resources/media/most_popular_banner_v2.jpg deleted file mode 100644 index 870c382..0000000 Binary files a/resources/media/most_popular_banner_v2.jpg and /dev/null differ diff --git a/resources/media/news_banner_v2.jpg b/resources/media/news_banner_v2.jpg deleted file mode 100644 index 2bdd922..0000000 Binary files a/resources/media/news_banner_v2.jpg and /dev/null differ diff --git a/resources/media/recently_added_banner_v2.jpg b/resources/media/recently_added_banner_v2.jpg deleted file mode 100644 index 6727b7a..0000000 Binary files a/resources/media/recently_added_banner_v2.jpg and /dev/null differ diff --git a/resources/media/schedule_banner_v2.jpg b/resources/media/schedule_banner_v2.jpg deleted file mode 100644 index d17f803..0000000 Binary files a/resources/media/schedule_banner_v2.jpg and /dev/null differ diff --git a/resources/media/search_banner_v2.jpg b/resources/media/search_banner_v2.jpg deleted file mode 100644 index 7c0194d..0000000 Binary files a/resources/media/search_banner_v2.jpg and /dev/null differ diff --git a/resources/media/shows_banner_v2.jpg b/resources/media/shows_banner_v2.jpg deleted file mode 100644 index 9d2af3e..0000000 Binary files a/resources/media/shows_banner_v2.jpg and /dev/null differ diff --git a/resources/media/tips_banner_v2.jpg b/resources/media/tips_banner_v2.jpg deleted file mode 100644 index 786c15b..0000000 Binary files a/resources/media/tips_banner_v2.jpg and /dev/null differ diff --git a/resources/media/topics_banner_v2.jpg b/resources/media/topics_banner_v2.jpg deleted file mode 100644 index bd5fbf3..0000000 Binary files a/resources/media/topics_banner_v2.jpg and /dev/null differ diff --git a/resources/media/trailer_banner_v2.jpg b/resources/media/trailer_banner_v2.jpg deleted file mode 100644 index 9a36963..0000000 Binary files a/resources/media/trailer_banner_v2.jpg and /dev/null differ diff --git a/resources/poster.jpg b/resources/poster.jpg new file mode 100644 index 0000000..d904e10 Binary files /dev/null and b/resources/poster.jpg differ diff --git a/resources/settings.xml b/resources/settings.xml index 2ec4e4f..3e73395 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,13 +1,19 @@ - - - - - - - - - + + + + + + + + + - + + + + + + + \ No newline at end of file